Last active
March 24, 2025 14:46
-
-
Save nmulasmajic/f90661489f858237bcd68fbde5516abd to your computer and use it in GitHub Desktop.
Discovers the base address of ntoskrnl when IDA's GDB stub is loaded by leveraging the IDT.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
''' | |
Module Name: | |
find_nt_imagebase_x64.py | |
Abstract: | |
Discovers the base address of ntoskrnl when IDA's GDB stub is | |
loaded by leveraging the IDT. | |
NOTE: This is only compatible for 64-bit editions of Windows. | |
Author: | |
Nemanja (Nemi) Mulasmajic <[email protected]> | |
http://triplefault.io | |
''' | |
from idaapi import * | |
# The size of a page on x86/AMD64. | |
PAGE_SIZE = 4096 | |
def splice(string, start_token, end_token): | |
''' | |
Given an input 'string', extracts the contents between the | |
starting and ending tokens. | |
''' | |
start_pos = string.find(start_token) | |
end_pos = string.rfind(end_token) | |
# This means our tokens are invalid and don't exist in the string. | |
if start_pos == -1 or end_pos == -1: | |
return None | |
start_pos += len(start_token) | |
# Can't splice the string if this is true. | |
if start_pos > end_pos: | |
return None | |
# Splices the string. | |
return string[start_pos:end_pos] | |
def read_idt_entry(address): | |
''' | |
Extracts the virtual address of the _KIDTENTRY64 at 'address'. | |
''' | |
# nt!_KIDTENTRY64 | |
''' | |
+0x000 OffsetLow : Uint2B | |
+0x002 Selector : Uint2B | |
+0x004 IstIndex : Pos 0, 3 Bits | |
+0x004 Reserved0 : Pos 3, 5 Bits | |
+0x004 Type : Pos 8, 5 Bits | |
+0x004 Dpl : Pos 13, 2 Bits | |
+0x004 Present : Pos 15, 1 Bit | |
+0x006 OffsetMiddle : Uint2B | |
+0x008 OffsetHigh : Uint4B | |
+0x00c Reserved1 : Uint4B | |
+0x000 Alignment : Uint8B | |
''' | |
# Relevant structure offsets. | |
OFFSET_KIDTENTRY64_OFFSETLOW = 0x0 | |
OFFSET_KIDTENTRY64_OFFSETMIDDLE = 0x6 | |
OFFSET_KIDTENTRY64_OFFSETHIGH = 0x8 | |
# Read the data. | |
OffsetLow = DbgWord(address + OFFSET_KIDTENTRY64_OFFSETLOW) | |
OffsetMiddle = DbgWord(address + OFFSET_KIDTENTRY64_OFFSETMIDDLE) | |
OffsetHigh = DbgDword(address + OFFSET_KIDTENTRY64_OFFSETHIGH) | |
# Failed to read some part of the offset. | |
if OffsetLow is None or OffsetMiddle is None or OffsetHigh is None: | |
return None | |
# Build the 64-bit address representing this structure. | |
return ((OffsetHigh << 32) + (OffsetMiddle << 16) + OffsetLow) | |
def page_align(address): | |
''' | |
Aligns the 'address' on an architecture page boundary (0x1000). | |
''' | |
return (address & ~(PAGE_SIZE - 1)) | |
def find_base_address(address, verbose = True): | |
''' | |
Walks memory backwards from the starting 'address' until a | |
valid PE header is located. | |
''' | |
# nt!_IMAGE_DOS_HEADER | |
''' | |
+0x000 e_magic : Uint2B | |
+0x002 e_cblp : Uint2B | |
+0x004 e_cp : Uint2B | |
+0x006 e_crlc : Uint2B | |
+0x008 e_cparhdr : Uint2B | |
+0x00a e_minalloc : Uint2B | |
+0x00c e_maxalloc : Uint2B | |
+0x00e e_ss : Uint2B | |
+0x010 e_sp : Uint2B | |
+0x012 e_csum : Uint2B | |
+0x014 e_ip : Uint2B | |
+0x016 e_cs : Uint2B | |
+0x018 e_lfarlc : Uint2B | |
+0x01a e_ovno : Uint2B | |
+0x01c e_res : [4] Uint2B | |
+0x024 e_oemid : Uint2B | |
+0x026 e_oeminfo : Uint2B | |
+0x028 e_res2 : [10] Uint2B | |
+0x03c e_lfanew : Int4B | |
''' | |
IMAGE_DOS_SIGNATURE = 0x5A4D # 'MZ' | |
# Relevant structure offsets. | |
OFFSET_IMAGE_DOS_HEADER_E_MAGIC = 0x0 | |
OFFSET_IMAGE_DOS_HEADER_E_LFANEW = 0x3c | |
# nt!_IMAGE_NT_HEADERS | |
''' | |
+0x000 Signature : Uint4B | |
+0x004 FileHeader : _IMAGE_FILE_HEADER | |
+0x018 OptionalHeader : _IMAGE_OPTIONAL_HEADER64 | |
''' | |
IMAGE_NT_SIGNATURE = 0x00004550 # 'PE00' | |
# Relevant structure offsets. | |
OFFSET_IMAGE_NT_HEADERS_SIGNATURE = 0x0 | |
# Find the page aligned offset of the specified symbol's address by | |
# stripping off the page RVA. | |
DosHeader = page_align(address) | |
if verbose: | |
print "\nSearching for base address of symbol @ {} ({}).".format(hex(address), hex(DosHeader)) | |
print "=" * 100 | |
while DosHeader != 0: | |
e_magic = DbgWord(DosHeader + OFFSET_IMAGE_DOS_HEADER_E_MAGIC) | |
# If we can't read the page, it's most likely invalid (not | |
# mapped in). In the kernel most PE images (like ntoskrnl) | |
# are more or less guaranteed to have their PE header in | |
# the NonPagedPool. We skip invalid pages here. | |
if e_magic is not None: | |
if verbose: | |
print "{} --> {}".format(hex(DosHeader), hex(e_magic)) | |
# Do we have an 'MZ'? | |
if e_magic == IMAGE_DOS_SIGNATURE: | |
# Extract the e_lfanew. | |
e_lfanew = DbgDword(DosHeader + OFFSET_IMAGE_DOS_HEADER_E_LFANEW) | |
# Go to the (potential) IMAGE_NT_HEADERS at this location. | |
NtHeaders = DosHeader + e_lfanew | |
# The IMAGE_NT_HEADERS should be on the same | |
# page as the IMAGE_DOS_HEADER. If this is not true, | |
# something's weird and we shouldn't read from this address. | |
if page_align(NtHeaders) == DosHeader: | |
Signature = DbgDword(NtHeaders + OFFSET_IMAGE_NT_HEADERS_SIGNATURE) | |
if verbose: | |
print "\t{} --> {}".format(hex(NtHeaders), hex(Signature)) | |
# Do we have a 'PE00'? | |
if Signature == IMAGE_NT_SIGNATURE: | |
if verbose: | |
print "\t{} Base address located @ {}.".format("^" * 50, hex(DosHeader)) | |
# At this point, it looks like we have both a valid | |
# DOS and NT header. This should be the right base | |
# address. | |
return DosHeader | |
# Try another page. | |
DosHeader -= PAGE_SIZE | |
# If we get to here... someone left this script running way too long. | |
return None | |
########################################################### | |
# Begin scripting logic. | |
########################################################### | |
print "=" * 100 | |
print "Discovers the base address of ntoskrnl when IDA's GDB stub is loaded by leveraging the IDT.\n" | |
print "NOTE: This is only compatible for 64-bit editions of Windows." | |
print "\t\t\t~ http://triplefault.io ~" | |
print "=" * 100 | |
# Ask for the idtr register from the VMware GDB stub. | |
monitor_result = SendDbgCommand("r idtr") | |
# The string is returned in the following format: | |
# idtr base=0xfffff800707c9070 limit=0xfff | |
try: | |
# Try to extract just the numerical base. | |
idt_base = int(splice(monitor_result, "base=", " limit"), 16) | |
except: | |
print "ERROR: Failed to retrieve IDT base from VMware's GDB stub." | |
exit(-1) | |
print "IDT base @ {}.".format(hex(idt_base)) | |
idt_entry = read_idt_entry(idt_base) | |
if idt_entry is None: | |
print "ERROR: Failed to extract and parse KIDTENTRY64." | |
exit(-2) | |
print "_KIDTENTRY64[0] (nt!KiDivideErrorFault) @ {}.".format(hex(idt_entry)) | |
# We have a symbol in the address space of nt!* (unless someone | |
# detoured the IDT entry...). At this point, we walk kernel | |
# memory backwards from the start of this symbol until we | |
# get to a valid PE header. This should be the base address of | |
# ntoskrnl. | |
ntoskrnl_base = find_base_address(idt_entry) | |
if ntoskrnl_base is not None: | |
print "\nThe base address of nt (ntoskrnl) is @ {}.".format(hex(ntoskrnl_base)) | |
else: | |
print "\nERROR: Could not find the base address of ntoskrnl after searching all resident memory. Something clearly went wrong. Additionally, you waited a very long time. Sorry!" | |
exit(-3) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment