rework demos

This commit is contained in:
cailllev
2026-06-02 22:02:25 +02:00
parent 6457ac0483
commit 1929469bab
11 changed files with 1332 additions and 31 deletions
+4 -1
View File
@@ -382,4 +382,7 @@ FodyWeavers.xsd
!/x64/Release/ETWDump.exe
!/x64/Release/InjectLoader.exe
!/x64/Release/ProcTerminator.exe
!/x64/Release/ReadPEB.exe
!/x64/Release/ReadPEB.exe
# pdb offsets
helpers/EDRSandblast/offsets/
+10
View File
@@ -17,6 +17,8 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "ProcInject", "attacks\ProcI
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "InjectLoader", "InjectLoader\InjectLoader.vcxproj", "{377AA798-10D4-47EA-A2BA-406DECE3DCB5}"
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "ETWDump", "misc\ETWDump\ETWDump.vcxproj", "{558A0A1C-D385-48EF-B2BD-D6381F8E551A}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|x64 = Debug|x64
@@ -73,6 +75,14 @@ Global
{377AA798-10D4-47EA-A2BA-406DECE3DCB5}.Release|x64.Build.0 = Release|x64
{377AA798-10D4-47EA-A2BA-406DECE3DCB5}.Release|x86.ActiveCfg = Release|Win32
{377AA798-10D4-47EA-A2BA-406DECE3DCB5}.Release|x86.Build.0 = Release|Win32
{558A0A1C-D385-48EF-B2BD-D6381F8E551A}.Debug|x64.ActiveCfg = Debug|x64
{558A0A1C-D385-48EF-B2BD-D6381F8E551A}.Debug|x64.Build.0 = Debug|x64
{558A0A1C-D385-48EF-B2BD-D6381F8E551A}.Debug|x86.ActiveCfg = Debug|Win32
{558A0A1C-D385-48EF-B2BD-D6381F8E551A}.Debug|x86.Build.0 = Debug|Win32
{558A0A1C-D385-48EF-B2BD-D6381F8E551A}.Release|x64.ActiveCfg = Release|x64
{558A0A1C-D385-48EF-B2BD-D6381F8E551A}.Release|x64.Build.0 = Release|x64
{558A0A1C-D385-48EF-B2BD-D6381F8E551A}.Release|x86.ActiveCfg = Release|Win32
{558A0A1C-D385-48EF-B2BD-D6381F8E551A}.Release|x86.Build.0 = Release|Win32
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
+2 -2
View File
@@ -93,10 +93,10 @@ int unload(int pid, std::string dllName) { // or just use a stopRequest.txt
if (!NT_SUCCESS(st)) {
DWORD winerr = g_origRtlNtStatusToDosError(st);
std::wcerr << "[!] InjectorLoader: Failt to NtOpenEvent " << eventName << ", WinErr = " << winerr << L"\n";
std::wcerr << "[!] InjectLoader: Failt to NtOpenEvent " << eventName << ", WinErr = " << winerr << L"\n";
}
else {
std::wcout << "[-] InjectorLoader: NtOpenEvent " << eventName << " ok, sending stop signal\n";
std::wcout << "[-] InjectLoader: NtOpenEvent " << eventName << " ok, sending stop signal\n";
SetEvent(hEvent);
CloseHandle(hEvent);
}
+17
View File
@@ -0,0 +1,17 @@
# py -m pip install requests pefile
$r = $PsScriptRoot
$edrs = "$r\..\helpers\EDRSandblast"
$eoffs = "$edrs\ExtractOffsets.py"
$offsets = "$edrs\offsets\"
if(!(Test-Path $offsets)) {
mkdir $offsets
}
copy C:\Windows\System32\ntoskrnl.exe $offsets
copy C:\Windows\System32\drivers\fltmgr.sys $offsets
py $eoffs ntoskrnl -i $offsets
py $eoffs fltmgr -i $offsets
mv "$r\NtoskrnlOffsets.csv" $offsets -Force
mv "$r\FltmgrOffsets.csv" $offsets -Force
+5 -2
View File
@@ -6,8 +6,11 @@ $monitorDLL = "$rel\EDRReflectiveHooker.dll"
$mdePID = (Get-Process -Name "MsMpEng").ID
# disable callbacks
$edrSandblast = "$root\helpers\EDRSandblast\EDRSandblast.exe"
$edrSandblastArgs = "toggle_callbacks 0e1 --kernelmode -i"
$edrs = "$root\helpers\EDRSandblast"
$edrSandblast = "$edrs\EDRSandblast.exe"
$ntO = "$edrs\offsets\NtoskrnlOffsets.csv"
$fltO = "$edrs\offsets\FltmgrOffsets.csv"
$edrSandblastArgs = "toggle_callbacks 0e1 --kernelmode --nt-offsets $ntO --fltmgr-offsets $fltO"
Start-Process $edrSandblast -Args $edrSandblastArgs
$_ = Read-Host "[*] Press ENTER when callbacks are disabled"
+6 -3
View File
@@ -7,13 +7,16 @@ $mdePID = (Get-Process -Name "MsMpEng").ID
# start ETW parser for hooks
$ETWDump = "$rel\ETWDump.exe"
Start-Process $ETWDump -Args "Hooks"
Start-Process $ETWDump -Args "Hooks minimal"
$_ = Read-Host "[*] Press ENTER to disable callbacks"
# disable callbacks
$edrSandblast = "$root\helpers\EDRSandblast\EDRSandblast.exe"
$edrSandblastArgs = "toggle_callbacks 0e1 --kernelmode -i"
$edrs = "$root\helpers\EDRSandblast"
$edrSandblast = "$edrs\EDRSandblast.exe"
$ntO = "$edrs\offsets\NtoskrnlOffsets.csv"
$fltO = "$edrs\offsets\FltmgrOffsets.csv"
$edrSandblastArgs = "toggle_callbacks 0e1 --kernelmode --nt-offsets $ntO --fltmgr-offsets $fltO"
Start-Process $edrSandblast -Args $edrSandblastArgs
$_ = Read-Host "[*] Press ENTER when callbacks are disabled"
+459
View File
@@ -0,0 +1,459 @@
import argparse
import csv
import os
from requests import get
from gzip import decompress
from json import loads
from concurrent.futures import ThreadPoolExecutor, as_completed
import threading
from lightpdbparser import Pdb
from pefile import PE, DIRECTORY_ENTRY, PEFormatError
THREADS_LIMIT = None
CSVLock = threading.Lock()
machineType = dict(x86=332, x64=34404)
supported_images = ["ntoskrnl.exe", "wdigest.dll", "ci.dll", "fltmgr.sys"]
modes = [image_name.split(".")[0] for image_name in supported_images]
extensions_by_mode = dict(image_name.split(".") for image_name in supported_images)
known_image_versions = {mode: list() for mode in modes}
modes_by_imagename = dict(zip(supported_images, modes))
csvFilenameByMode = {mode: mode.capitalize() + "Offsets.csv" for mode in modes}
symbols = dict(
ntoskrnl=[
("PspCreateProcessNotifyRoutine", "symbol"),
("PspCreateThreadNotifyRoutine", "symbol"),
("PspLoadImageNotifyRoutine", "symbol"),
("_EPROCESS", "Protection", "field"),
("EtwThreatIntProvRegHandle", "symbol"),
("_ETW_REG_ENTRY", "GuidEntry", "field"),
("_ETW_GUID_ENTRY", "ProviderEnableInfo", "field"),
("PsProcessType", "symbol"),
("PsThreadType", "symbol"),
("_OBJECT_TYPE", "CallbackList", "field"),
("SeCiCallbacks", "symbol"),
],
wdigest=[
("g_fParameter_UseLogonCredential", "symbol"),
("g_IsCredGuardEnabled", "symbol"),
],
ci=[
("g_CiOptions", "symbol"),
("CiValidateImageHeader", "symbol"),
],
fltmgr=[
("FltGlobals", "symbol"),
("_GLOBALS", "FrameList", "field"),
("_FLT_RESOURCE_LIST_HEAD", "rList", "field"),
("_FLTP_FRAME", "Links", "field"),
("_FLTP_FRAME", "RegisteredFilters", "field"),
("_FLT_OBJECT", "PrimaryLink", "field"),
("_FLT_FILTER", "DriverObject", "field"),
("_FLT_FILTER", "InstanceList", "field"),
("_DRIVER_OBJECT", "DriverInit", "field"),
("_FLT_INSTANCE", "CallbackNodes", "field"),
("_FLT_INSTANCE", "FilterLink", "field"),
],
)
symbols_names = {mode: [t[0] if t[-1] == "symbol" else f"{t[0]}_{t[1]}" for t in symbols[mode]] for mode in modes}
def find(key: str, d: dict):
for k, v in d.items():
if k == key:
return v
if isinstance(v, dict):
return find(key, v)
return None
def printl(s, lock, **kwargs):
with lock:
print(s, **kwargs)
def downloadSpecificFile(entry, pe_basename, pe_ext, knownPEVersions, output_folder, lock):
pe_name = f"{pe_basename}.{pe_ext}"
if "fileInfo" not in entry:
# printl(f'[!] Entry {pe_hash} has no fileInfo, skipping it.', lock)
return "SKIP"
if "timestamp" not in entry["fileInfo"]:
# printl(f'[!] Entry has no timestamp, skipping it.', lock)
return "SKIP"
timestamp = entry["fileInfo"]["timestamp"]
if "virtualSize" not in entry["fileInfo"]:
# printl(f'[!] Entry has no virtualSize, skipping it.', lock)
return "SKIP"
if "machineType" not in entry["fileInfo"] or entry["fileInfo"]["machineType"] != machineType["x64"]:
# printl('No machine Type', lock)
return "SKIP"
virtual_size = entry["fileInfo"]["virtualSize"]
file_id = hex(timestamp).replace("0x", "").zfill(8).upper() + hex(virtual_size).replace("0x", "").upper()
url = "https://msdl.microsoft.com/download/symbols/" + pe_name + "/" + file_id + "/" + pe_name
try:
version = entry["fileInfo"]["version"].split(" ")[0]
except KeyError:
version = find("version", entry).split(" ")[0]
if version and version.count(".") != 3:
version = None
if not version:
printl(f"[*] Error parsing version", lock)
return "SKIP"
# Output file format: <PE>_build-revision.<exe | dll>
output_version = "-".join(version.split(".")[-2:])
output_file = f"{pe_basename}_{output_version}.{pe_ext}"
# If the PE version is already known, skip download.
if output_file in knownPEVersions:
printl(f"[*] Skipping download of known {pe_name} version: {output_file}", lock)
return "SKIP"
output_file_path = os.path.join(output_folder, output_file)
if os.path.isfile(output_file_path):
printl(f"[*] Skipping {output_file_path} which already exists", lock)
return "SKIP"
# printl(f'[*] Downloading {pe_name} version {version}... ', lock)
try:
peContent = get(url)
if len(peContent.content) == 0:
printl(f"[*] Skipping {output_file_path} which is empty on MS server", lock)
return "SKIP"
with open(output_file_path, "wb") as f:
f.write(peContent.content)
printl(
f"[+] Finished download of {pe_name} version {version} (file: {output_file})!",
lock,
)
return "OK"
except Exception as e:
printl(
f"[!] ERROR : Could not download {pe_name} version {version} (URL: {url}): {str(e)}.",
lock,
)
return "KO"
def downloadPEFileFromMS(pe_basename, pe_ext, knownPEVersions, output_folder):
pe_name = f"{pe_basename}.{pe_ext}"
print(f"[*] Downloading {pe_name} files!")
pe_json_gz = get(f"https://winbindex.m417z.com/data/by_filename_compressed/{pe_name}.json.gz").content
pe_json = decompress(pe_json_gz)
pe_list = loads(pe_json)
i = 0
futures = set()
lock = threading.Lock()
with ThreadPoolExecutor(max_workers=THREADS_LIMIT) as executor:
for pe_hash in pe_list:
entry = pe_list[pe_hash]
futures.add(
executor.submit(
downloadSpecificFile,
entry,
pe_basename,
pe_ext,
knownPEVersions,
output_folder,
lock,
)
)
for future in as_completed(futures):
printl(f"{i + 1}/{len(pe_list)}", lock, end="\r")
i += 1
def get_symbol_offset(symbols_info, symbol_name):
for line in symbols_info:
# sometimes, a "_" is prepended to the symbol name ...
if line.strip().split(" ")[-1].endswith(symbol_name):
return int(line.split(" ")[0], 16)
else:
return 0
def get_field_offset(symbols_info, field_name):
for line in symbols_info:
if field_name in line:
assert "offset" in line
symbol_offset = int(line.split("+")[-1], 16)
return symbol_offset
else:
return 0
def get_file_version(path):
pe = PE(path, fast_load=True)
pe.parse_data_directories(directories=[DIRECTORY_ENTRY["IMAGE_DIRECTORY_ENTRY_RESOURCE"]])
if not "VS_FIXEDFILEINFO" in pe.__dict__ or not pe.VS_FIXEDFILEINFO:
raise RuntimeError("Version info not found in {pename}")
verinfo = pe.VS_FIXEDFILEINFO[0]
filever = (
verinfo.FileVersionMS >> 16,
verinfo.FileVersionMS & 0xFFFF,
verinfo.FileVersionLS >> 16,
verinfo.FileVersionLS & 0xFFFF,
)
return filever
# Takes a path to a PE file as argument, download the associated PDB
# Return the path of the existing PDB if any, and the content of the PDB in memory
# use keep_ondisk=False not to store the PDB files on disk
def get_pdb(pe: PE, pe_path, keep_ondisk=True, verbose=False):
pdb_file_path = pe_path.rsplit(".", maxsplit=1)[0] + ".pdb"
if not os.path.isfile(pdb_file_path):
if verbose:
print(f"[*] Downloading missing {pdb_file_path}")
pe.parse_data_directories(directories=[DIRECTORY_ENTRY["IMAGE_DIRECTORY_ENTRY_DEBUG"]])
guid_string = (
f"{pe.DIRECTORY_ENTRY_DEBUG[0].entry.Signature_Data1:08X}"
+ f"{pe.DIRECTORY_ENTRY_DEBUG[0].entry.Signature_Data2:04X}"
+ f"{pe.DIRECTORY_ENTRY_DEBUG[0].entry.Signature_Data3:04X}"
+ f"{pe.DIRECTORY_ENTRY_DEBUG[0].entry.Signature_Data4:02X}"
+ f"{pe.DIRECTORY_ENTRY_DEBUG[0].entry.Signature_Data5:02X}"
+ pe.DIRECTORY_ENTRY_DEBUG[0].entry.Signature_Data6.hex().upper()
)
age_string = f"{pe.DIRECTORY_ENTRY_DEBUG[0].entry.Age:X}"
pdb_filename = pe.DIRECTORY_ENTRY_DEBUG[0].entry.PdbFileName.decode().replace("\x00", "")
pdb_url = f"https://msdl.microsoft.com/download/symbols/{pdb_filename}/{guid_string}{age_string}/{pdb_filename}"
try:
pdbContent = get(pdb_url)
if len(pdbContent.content) == 0:
raise ValueError("Downloaded PDB is empty")
if keep_ondisk:
with open(pdb_file_path, "wb") as f:
f.write(pdbContent.content)
if verbose:
print(f"[+] Finished download PDB of {pe_path} version (file: {pdb_file_path})!")
return pdb_file_path, pdbContent.content
if not keep_ondisk:
return None, pdbContent.content
except Exception as e:
print(f"[!] ERROR : Could not download PDB of {pe_path} (URL: {pdb_url}): {str(e)}.")
return None, None
elif os.path.isfile(pdb_file_path):
pdb = Pdb(path=pdb_file_path)
pe.parse_data_directories(directories=[DIRECTORY_ENTRY["IMAGE_DIRECTORY_ENTRY_DEBUG"]])
guid_string = (
f"{pe.DIRECTORY_ENTRY_DEBUG[0].entry.Signature_Data1:08x}-"
+ f"{pe.DIRECTORY_ENTRY_DEBUG[0].entry.Signature_Data2:04x}-"
+ f"{pe.DIRECTORY_ENTRY_DEBUG[0].entry.Signature_Data3:04x}-"
+ f"{pe.DIRECTORY_ENTRY_DEBUG[0].entry.Signature_Data4:02x}"
+ f"{pe.DIRECTORY_ENTRY_DEBUG[0].entry.Signature_Data5:02x}-"
+ pe.DIRECTORY_ENTRY_DEBUG[0].entry.Signature_Data6.hex().lower()
)
if str(pdb.PDBStream.Guid).lower() != guid_string:
print(f"[!] ERROR : PDB {pdb_file_path} does match {pe_path}. Redownloading")
del pdb
os.remove(pdb_file_path)
return get_pdb(pe, pe_path, keep_ondisk, verbose)
return pdb_file_path, None
def extractOffsets(input_file, output_file, mode):
if os.path.isfile(input_file):
try:
# check image type (ntoskrnl, wdigest, etc.)
pe = PE(input_file, fast_load=True)
export_directory_entry = pe.OPTIONAL_HEADER.DATA_DIRECTORY[DIRECTORY_ENTRY["IMAGE_DIRECTORY_ENTRY_EXPORT"]]
export_directory_rva = export_directory_entry.VirtualAddress
image_name_rva = pe.get_dword_at_rva(export_directory_rva + 3 * 4)
name = pe.get_string_at_rva(image_name_rva).decode().lower()
for image_name in supported_images:
if image_name in name:
imageType = modes_by_imagename[image_name]
break
else:
print(f"[*] File {input_file} unrecognized")
return
# todo : remove this and make a unique function
if mode != imageType:
print(f"[*] Skipping {input_file} since we are in {mode} mode")
return
if os.path.sep not in input_file:
input_file = "." + os.path.sep + input_file
full_version = get_file_version(input_file)
# Checks if the image version is already present in the CSV
extension = extensions_by_mode[imageType]
imageVersion = f"{imageType}_{full_version[2]}-{full_version[3]}.{extension}"
if imageVersion in known_image_versions[imageType]:
print(f"[*] Skipping known {imageType} version {imageVersion} (file: {input_file})")
try:
"""
Sometimes, PEs with different versions have the same exact code (only the version and the signature
change). They even have the same PE "timestamp" (which is a build hash in reality) and same virtual
size. So the download links on MS servers are the same. That's why winbindex sometimes shows the
metadata of one version of a PE, and the downloaded version is different.
Anyway, the offsets are identical between versions affected by these "collisions".
"""
input_file_basename = os.path.basename(input_file)
if not input_file_basename.startswith(f"{imageType}_"):
return
if not input_file_basename.endswith(f".{extension}"):
return
if not input_file_basename.count("-") == 1:
return
version_2, version_3 = tuple(
int(part)
for part in input_file_basename[len(f"{imageType}_") : -len(f".{extension}")].split("-")
)
imageVersion = input_file_basename
if imageVersion in known_image_versions[imageType]:
return
print("\r", end="") # Not skipping after all
except ValueError:
return
except KeyError:
return
# print(f'[*] Processing {imageType} version {imageVersion} (file: {input_file})')
# download the PDB if needed
pdb_path, pdb_content = get_pdb(pe, input_file, verbose=True)
pdb = Pdb(path=pdb_path, content=pdb_content)
symbols_values = list()
for *symbol_name, offset_type in symbols[imageType]:
if offset_type == "symbol":
get_offset = pdb.get_symbol_offset
elif offset_type == "field":
get_offset = pdb.get_field_offset
else:
raise ValueError
symbol_value = get_offset(*symbol_name)
if symbol_value is None:
symbol_value = 0
symbols_values.append(symbol_value)
# print(f"[+] {symbol_name} = {hex(symbol_value)}")
with CSVLock:
with open(output_file, "a") as output:
output.write(f'{imageVersion},{",".join(hex(val).replace("0x","") for val in symbols_values)}\n')
# print("wrote into CSV !")
del pdb
known_image_versions[imageType].append(imageVersion)
print(f"[+] Finished processing of {imageType} {input_file}!")
except PEFormatError as e:
# file is not a PE
if not input_file.endswith(".pdb"):
print(f"[!] ERROR : Could not process file {input_file}: not a valid PE")
except Exception as e:
print(f"[!] ERROR : Could not process file {input_file}.")
print(f"[!] Error message: {e}")
raise e
elif os.path.isdir(input_file):
print(f"[*] Processing folder: {input_file}")
with ThreadPoolExecutor(max_workers=THREADS_LIMIT) as extractorPool:
args = [(os.path.join(input_file, file), output_file, mode) for file in os.listdir(input_file)]
for i, res in enumerate(extractorPool.map(extractOffsets, *zip(*args))):
print(f"{i + 1}/{len(args)}", end="\r")
print(f"[+] Finished processing of folder {input_file}!")
else:
print(f"[!] ERROR : The specified input {input_file} is neither a file nor a directory.")
def loadOffsetsFromCSV(loadedVersions, CSVPath):
print(f'[*] Loading the known known PE versions from "{CSVPath}".')
with open(CSVPath, "r") as csvFile:
csvReader = csv.reader(csvFile, delimiter=",")
next(csvReader)
for peLine in csvReader:
loadedVersions.append(peLine[0])
def sortOutputFile(csvFile):
def lineKey(line):
major = int(line.split("_")[1].split("-")[0])
minor = int(line.split("-")[1].split(".")[0])
return (major, minor)
with open(csvFile) as f:
header_line = f.readline()
content = f.readlines()
with open(csvFile, "w") as f:
f.write(header_line)
f.writelines(sorted(set(content), key=lineKey))
if __name__ == "__main__":
parser = argparse.ArgumentParser()
modes_str = "/".join(known_image_versions)
files = " / ".join(modes_by_imagename)
csvfiles = " / ".join(csvFilenameByMode.values())
parser.add_argument(
"mode",
help=f"{modes_str}. Mode to download and extract offsets from either {files}",
)
parser.add_argument(
"-i",
"--input",
dest="input",
required=True,
help=f"Single file or directory containing {files} to extract offsets from. If in download mode, the PE downloaded from MS symbols servers will be placed in this folder.",
)
parser.add_argument(
"-o",
"--output",
dest="output",
help=f"CSV file to write offsets to. If the specified file already exists, only new ntoskrnl versions will be downloaded / analyzed. Defaults to {csvfiles} in the current folder.",
)
parser.add_argument(
"-d",
"--download",
dest="download",
action="store_true",
help="Flag to download the PE from Microsoft servers using list of versions from winbindex.m417z.com.",
)
args = parser.parse_args()
mode = args.mode.lower()
if mode not in known_image_versions:
print(f'[!] ERROR : unsupported mode "{args.mode}", supported mode are: {modes}')
exit(1)
# If the output file exists, load the already analyzed image versions.
# Otherwise, write CSV headers to the new file.
if not args.output:
args.output = csvFilenameByMode[mode]
if os.path.isfile(args.output):
loadOffsetsFromCSV(known_image_versions[mode], args.output)
print(f'[+] Loaded {len(known_image_versions[mode])} known {mode} versions from "{args.output}"')
else:
with open(args.output, "w") as output:
output.write(mode + "Version," + ",".join(elem for elem in symbols_names[mode]) + "\n")
# In download mode, an updated list of image versions published will be retrieved from https://winbindex.m417z.com.
# The symbols for each version will be downloaded from the Microsoft symbols servers.
# Only new versions will be downloaded if the specified output file already contains offsets.
if args.download:
if not os.path.isdir(args.input):
print("[!] ERROR : in download mode, -i / --input option must specify a folder")
exit(1)
extension = extensions_by_mode[mode]
downloadPEFileFromMS(mode, extension, known_image_versions[mode], args.input)
# Extract the offsets from the specified file or the folders containing image files.
extractOffsets(args.input, args.output, mode)
sortOutputFile(args.output)
+791
View File
@@ -0,0 +1,791 @@
#!/usr/bin/python3
"""
A python native parser with (many) missing features.
Only support the bare minimum to extract symbols addresses and field offsets in structures
Written from info found here: https://llvm.org/docs/PDB/index.html
"""
from math import ceil
from struct import unpack
from functools import cache, cached_property
from uuid import UUID
try:
from line_profiler_pycharm import profile
except ImportError:
profile = lambda x: x
def u32f(f, addr=None):
if addr is not None:
f.seek(addr)
return unpack("<I", f.read(4))[0]
def readat(f, addr, size):
f.seek(addr)
return f.read(size)
class MsfStream(object):
def __init__(self, msf, size, blocks):
self.msf = msf
self.size = size
self.blocks = blocks
self.cursor = 0
@profile
def read(self, size=None):
if size is not None:
size = min(self.size - self.cursor, size)
else:
size = self.size - self.cursor
content = b""
block_size = self.msf.BlockSize
current_block_index = self.cursor // block_size
while size:
current_block = self.blocks[current_block_index]
current_block_index += 1
block_offset = self.cursor % block_size
to_read = min(block_size - block_offset, size)
self.msf.f.seek(block_size * current_block + block_offset)
content += self.msf.f.read(to_read)
self.cursor += to_read
size -= to_read
return content
def seek(self, pos):
self.cursor = pos
def peek_u8(self, at=None):
pos = self.cursor
u = self.u8(at)
self.cursor = pos
return u
def peek_u16(self, at=None):
pos = self.cursor
u = self.u16(at)
self.cursor = pos
return u
def peek_u32(self, at=None):
pos = self.cursor
u = self.u32(at)
self.cursor = pos
return u
def u8(self, addr=None):
if addr is not None:
self.seek(addr)
return self.read(1)[0]
def u16(self, addr=None):
if addr is not None:
self.seek(addr)
return unpack("<H", self.read(2))[0]
def u32(self, addr=None):
if addr is not None:
self.seek(addr)
return unpack("<I", self.read(4))[0]
def u64(self, addr=None):
if addr is not None:
self.seek(addr)
return unpack("<Q", self.read(8))[0]
def cstring(self):
s = b""
start = self.cursor
while b"\x00" not in s:
s += self.read(32)
s = s.split(b"\x00", maxsplit=1)[0]
self.cursor = start + len(s) + 1
return s
class MsfStreamDirectory(object):
def __init__(self, msf):
self.msf = msf
# @cache
def __getitem__(self, num_dword):
StreamDirectoryBlockMapAddr = self.msf.BlockMapAddr * self.msf.BlockSize
block_number = num_dword * 4 // self.msf.BlockSize
block_addr = self.msf.BlockSize * u32f(self.msf.f, StreamDirectoryBlockMapAddr + 4 * block_number)
dword_addr = block_addr + (num_dword * 4) % self.msf.BlockSize
return u32f(self.msf.f, dword_addr)
@cached_property
def NumStreams(self):
return self[0]
def StreamSize(self, stream_number):
return self[1 + stream_number]
def StreamBlocks(self, stream_number):
index_streamblocks = 1 + self.NumStreams
for i in range(stream_number):
index_streamblocks += ceil(self.StreamSize(i) / self.msf.BlockSize)
blocks = [
self[index_streamblocks + b] for b in range(ceil(self.StreamSize(stream_number) / self.msf.BlockSize))
]
return blocks
class PdbInfoStream(MsfStream):
"""
struct PdbStreamHeader {
ulittle32_t Version;
ulittle32_t Signature;
ulittle32_t Age;
Guid UniqueId;
};
//Named stream hashmap
// "The on-disk layout of the Named Stream Map consists of 2 components. The first is a buffer of string data prefixed
// by a 32-bit length. The second is a serialized hash table whose key and value types are both uint32_t. The key is
// the offset of a null-terminated string in the string data buffer specifying the name of the stream, and the value
// is the MSF stream index of the stream with said name. Note that although the key is an integer, the hash function
// used to find the right bucket hashes the string at the corresponding offset in the string data buffer."
.--------------------.-- +0
| Size |
.--------------------.-- +4
| Capacity |
.--------------------.-- +8
| Present Bit Vector |
.--------------------.-- +N
| Deleted Bit Vector |
.--------------------.-- +M ─╮
| Key | │
.--------------------.-- +M+4 │
| Value | │
.--------------------.-- +M+4+sizeof(Value) │
... ├─ |Capacity| Bucket entries
.--------------------. │
| Key | │
.--------------------. │
| Value | │
.--------------------. ─╯
//+ a sequence of
enum class PdbRaw_FeatureSig : uint32_t {
VC110 = 20091201,
VC140 = 20140508,
NoTypeMerge = 0x4D544F4E,
MinimalDebugInfo = 0x494E494D,
};
"""
@cached_property
def Version(self):
return self.u32(0)
@cached_property
def Signature(self):
return self.u32(4)
@cached_property
def Age(self):
return self.u32(8)
@cached_property
def Guid(self):
return UUID(bytes_le=readat(self, 12, 16))
"""
Format explained here: https://github.com/willglynn/pdb/blob/b052964e09d03eb190c8a60dc76344150ff8a9df/src/pdbi.rs#L99
"""
@cached_property
def NamedStreamMap(self):
string_buffer_size = self.u32(3 * 4 + 16)
strings_buffer = self.read(string_buffer_size)
size_hashmap = self.u32()
capacity_hashmap = self.u32() # unused
present_bit_vector_word_count = self.u32()
present_bit_vector = 0
for i in range(present_bit_vector_word_count):
present_bit_vector |= self.u32() << (32 * i)
deleted_bit_vector_word_count = self.u32()
deleted_bit_vector = 0
for i in range(deleted_bit_vector_word_count):
deleted_bit_vector |= self.u32() << (32 * i)
named_streams_ids = dict()
count_present = 0
for i in range(capacity_hashmap):
if present_bit_vector & (1 << i):
key = self.u32()
value = self.u32()
count_present += 1
if not (deleted_bit_vector & (1 << i)):
assert key == 0 or strings_buffer[key - 1 : key] == b"\x00"
stream_name = strings_buffer[key:].split(b"\x00")[0]
stream_id = value
named_streams_ids[stream_name.decode()] = self.msf.Stream(stream_id)
assert count_present == size_hashmap
return named_streams_ids
class SymRecordStream(MsfStream):
# complete with https://github.com/microsoft/microsoft-pdb/blob/805655a28bd8198004be2ac27e6e0290121a5e89/include/cvinfo.h#L2900
# if a value is missing
REC_TYPES = {
0x110E: "S_PUB32", # a public symbol (CV internal reserved)
0x1125: "S_PROCREF", # Reference to a procedure
0x1127: "S_LPROCREF", # Local Reference to a procedure
0x1128: "S_ANNOTATIONREF", # Reference to an S_ANNOTATION symbol
}
def __init__(self, msf, size, blocks):
MsfStream.__init__(self, msf, size, blocks)
self.symbols = dict()
self.next_to_parse_offset = 0
def __iter__(self):
self.cursor = 0
return self
def __next__(self):
offset = None
while offset is None:
if self.cursor == self.size:
raise StopIteration
if self.size - self.cursor < 4:
raise ValueError
record_length = self.u16()
record_end = self.cursor + record_length
record_type = self.u16()
if self.size - self.cursor < record_length - 2:
raise ValueError
match self.REC_TYPES[record_type]:
case "S_PUB32":
flags, offset, segment = unpack("<IIH", self.read(10))
name = self.cstring()
self.cursor = record_end
return "S_PUB32", offset, name, segment
case "S_LPROCREF" | "S_PROCREF":
"""
sumName = self.u32() # SUC of the name
ibSym = offset = self.u32() # Offset of actual symbol in $$Symbols
imod = self.u16() # Module containing the actual symbol
name = self.read(record_length - 12)
# ignore for the moment
"""
offset = name = None
case "S_ANNOTATIONREF":
offset = name = None
case _:
offset = name = None
raise ValueError(f"{self.REC_TYPES[record_type]} : not implemented")
self.seek(record_end)
def search_and_cache_symbols(self, symbolname: str):
symbolname_raw = symbolname.encode()
if symbolname_raw not in self.symbols:
saved_cursor = self.cursor
self.cursor = self.next_to_parse_offset
while self.cursor != self.size:
try:
_, offset, name, segment = self.__next__()
except StopIteration:
continue
self.symbols[name] = (offset, segment)
if name == symbolname_raw:
break
else:
return (None, None)
self.next_to_parse_offset = self.cursor
self.cursor = saved_cursor
return self.symbols[symbolname_raw]
class DBIStream(MsfStream):
"""
struct DbiStreamHeader {
int32_t VersionSignature; // 0
uint32_t VersionHeader; // 4
uint32_t Age; // 8
uint16_t GlobalStreamIndex; // 12
uint16_t BuildNumber; // 14
uint16_t PublicStreamIndex; // 16
uint16_t PdbDllVersion; // 18
uint16_t SymRecordStream; // 20
uint16_t PdbDllRbld; // 22
int32_t ModInfoSize; // 24
int32_t SectionContributionSize; // 28
int32_t SectionMapSize; // 32
int32_t SourceInfoSize; // 36
int32_t TypeServerMapSize; // 40
uint32_t MFCTypeServerIndex; // 44
int32_t OptionalDbgHeaderSize; // 48
int32_t ECSubstreamSize; // 52
uint16_t Flags; // 56
uint16_t Machine; // 58
uint32_t Padding; // 60
};
"""
@cached_property
def SymRecordStream(self):
stream_id = self.peek_u16(20)
return SymRecordStream(
self.msf,
self.msf.StreamDirectory.StreamSize(stream_id),
self.msf.StreamDirectory.StreamBlocks(stream_id),
)
@cached_property
def ModInfoSize(self):
return self.peek_u32(24)
@cached_property
def SectionContributionSize(self):
return self.peek_u32(28)
@cached_property
def SectionMapSize(self):
return self.peek_u32(32)
@cached_property
def SourceInfoSize(self):
return self.peek_u32(36)
@cached_property
def TypeServerMapSize(self):
return self.peek_u32(40)
@cached_property
def OptionalDbgHeaderSize(self):
return self.peek_u32(48)
@cached_property
def ECSubstreamSize(self):
return self.peek_u32(52)
@cached_property
def SectionHeadersStream(self):
"""
See https://llvm.org/docs/PDB/DbiStream.html#optional-debug-header-stream
"""
if self.OptionalDbgHeaderSize // 2 < 6:
raise ValueError("OptionalDbgHeader not present or does not contain Section Header Data")
stream_id = self.peek_u16(
64 # DBI Header size
+ self.ModInfoSize
+ self.SectionContributionSize
+ self.SectionMapSize
+ self.SourceInfoSize
+ self.TypeServerMapSize
+ self.ECSubstreamSize
+ 0 # Optional Debug Header Stream starts here
+ 2 * 5 # uint16_t DbgStreamArray[5] contains the stream number of the section headers
)
return SectionHeaderStream(
self.msf, self.msf.StreamDirectory.StreamSize(stream_id), self.msf.StreamDirectory.StreamBlocks(stream_id)
)
class SectionHeaderStream(MsfStream):
"""
typedef struct _IMAGE_SECTION_HEADER {
BYTE Name[8];
union {
DWORD PhysicalAddress;
DWORD VirtualSize;
} Misc;
DWORD VirtualAddress;
DWORD SizeOfRawData;
DWORD PointerToRawData;
DWORD PointerToRelocations;
DWORD PointerToLinenumbers;
WORD NumberOfRelocations;
WORD NumberOfLinenumbers;
DWORD Characteristics;
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
"""
class SectionHeader(object):
def __init__(self, data):
(
self.Name,
self.VirtualSize,
self.VirtualAddress,
self.SizeOfRawData,
self.PointerToRawData,
self.PointerToRelocations,
self.PointerToLinenumbers,
self.NumberOfRelocations,
self.NumberOfLinenumbers,
self.Characteristics,
) = unpack("8sIIIIIIHHI", data)
@cached_property
def NumberOfSections(self):
assert self.size % 40 == 0
return self.size // 40
def __iter__(self):
self.cursor = 0
return self
def __next__(self):
if self.cursor >= self.size:
raise StopIteration
return SectionHeaderStream.SectionHeader(self.read(40))
def __getitem__(self, section_index):
if section_index >= self.NumberOfSections:
raise ValueError(f"Section number {section_index} does not exist")
self.cursor = section_index * 40
return SectionHeaderStream.SectionHeader(self.read(40))
class TPIorIPStream(MsfStream):
"""
struct TpiStreamHeader {
uint32_t Version;
uint32_t HeaderSize;
uint32_t TypeIndexBegin;
uint32_t TypeIndexEnd;
uint32_t TypeRecordBytes;
uint16_t HashStreamIndex;
uint16_t HashAuxStreamIndex;
uint32_t HashKeySize;
uint32_t NumHashBuckets;
int32_t HashValueBufferOffset;
uint32_t HashValueBufferLength;
int32_t IndexOffsetBufferOffset;
uint32_t IndexOffsetBufferLength;
int32_t HashAdjBufferOffset;
uint32_t HashAdjBufferLength;
};
"""
REC_TYPES = {
0x1001: "LF_MODIFIER",
0x1002: "LF_POINTER",
0x1008: "LF_PROCEDURE",
0x1201: "LF_ARGLIST",
0x1203: "LF_FIELDLIST",
0x1205: "LF_BITFIELD",
0x1404: "LF_INDEX",
0x1502: "LF_ENUMERATE",
0x1503: "LF_ARRAY",
0x1505: "LF_STRUCTURE",
0x1506: "LF_UNION",
0x1507: "LF_ENUM",
0x150D: "LF_MEMBER",
0x1605: "LF_STRING_ID",
0x1606: "LF_UDT_SRC_LINE",
}
def __init__(self, msf, size, blocks):
MsfStream.__init__(self, msf, size, blocks)
self.filter = None
self.type_index = self.TypeIndexBegin
self.types = dict()
self.REC_TYPES_ids = {self.REC_TYPES[k]: k for k in self.REC_TYPES}
self.types_parsed = False
@cached_property
def HeaderSize(self):
return self.u32(4)
@cached_property
def TypeIndexBegin(self):
return self.u32(8)
@cached_property
def TypeRecordBytes(self):
return self.u32(16)
def skip_padding(self):
b = self.u8()
self.cursor -= 1
if b in (0xF1, 0xF2, 0xF3):
padding_size = b & 0xF
# assert b"\xF3\xF2\xF1".endswith(self.read(padding_size))
self.cursor += padding_size
def unsigned(self):
leaf = self.u16()
if leaf < 0x8000:
return leaf
match leaf:
case 0x8000: # LF_CHAR
return self.u8()
case 0x8002: # LF_SHORT
return self.u16()
case 0x8003 | 0x8004: # LF_LONG |LF_ULONG
return self.u32()
case 0x800A: # LF_SHORT
return self.u64()
case _:
raise ValueError
def __iter__(self):
self.type_index = self.TypeIndexBegin
self.cursor = self.HeaderSize
return self
def __next__(self):
leaf_entry = None
while leaf_entry is None:
if self.cursor == self.size:
self.types_parsed = True
raise StopIteration
if self.size - self.cursor < 4:
raise ValueError
record_length = self.u16()
record_end = self.cursor + record_length
if self.size < record_end:
raise ValueError
if self.filter is not None and self.peek_u16() not in self.filter:
self.cursor = record_end
self.type_index += 1
continue
leaf_entry = self.parse_one_leaf_entry(record_end)
self.types[self.type_index] = leaf_entry
self.type_index += 1
if self.cursor > record_end:
raise ValueError
if self.cursor < record_end:
end = self.read(record_end - self.cursor)
if not b"\xf3\xf2\xf1".endswith(end):
raise ValueError(f"Unparsed data: {end} for record {leaf_entry}")
return leaf_entry
def parse_one_leaf_entry(self, record_end):
record_type = self.u16()
if record_type not in self.REC_TYPES:
raise ValueError(f"Record {hex(record_type)} not handled")
match self.REC_TYPES.get(record_type, "???"):
case "LF_MODIFIER":
utype = self.u32()
modifier = self.u16()
record = (utype, modifier)
case "LF_POINTER":
utype = self.u32()
attr = self.u32()
if ((attr >> 5) & 7) in (2, 3): # ptrmode == Member or MemberFunction
raise ValueError
record = (utype, attr)
case "LF_STRUCTURE":
count = self.u16()
properties = self.u16()
has_unique_name = (properties & 0x200) != 0
fields = self.u32()
derived_from = self.u32()
vtable_shape = self.u32()
size = self.unsigned()
name = self.cstring()
unique_name = self.cstring() if has_unique_name else None
record = (
count,
properties,
fields,
derived_from,
vtable_shape,
size,
name,
)
case "LF_FIELDLIST":
fields = list()
continuation = None
while self.cursor < record_end:
next_field = self.u16()
if self.REC_TYPES[next_field] == "LF_INDEX":
continuation = self.u32()
else:
self.cursor -= 2
fields.append(self.parse_one_leaf_entry(record_end))
self.skip_padding()
record = (fields, continuation)
case "LF_MEMBER":
attributes = self.u16()
field_type = self.u32()
offset = self.unsigned()
name = self.cstring()
record = (attributes, field_type, offset, name)
case "LF_ARGLIST":
count = self.u32()
arglist = [self.u32() for _ in range(count)]
record = arglist
case "LF_PROCEDURE":
return_type = self.u32()
attributes = self.u16()
parameter_count = self.u16()
argument_list = self.u32()
record = (return_type, attributes, parameter_count, argument_list)
case "LF_ARRAY":
element_type = self.u32()
indexing_type = self.u32()
size = self.unsigned()
pad = self.cstring()
assert pad == b""
record = (element_type, indexing_type, size)
case "LF_UNION":
count = self.u16()
properties = self.u16()
has_unique_name = (properties & 0x200) != 0
fields = self.u32()
size = self.unsigned()
name = self.cstring()
unique_name = self.cstring() if has_unique_name else None
record = (
count,
properties,
fields,
size,
name,
)
case "LF_ENUMERATE":
attributes = self.u16()
value = self.unsigned()
name = self.cstring()
record = (attributes, value, name)
case "LF_ENUM":
count = self.u16()
properties = self.u16()
has_unique_name = (properties & 0x200) != 0
underlying_type = self.u32()
fields = self.u32()
name = self.cstring()
unique_name = self.cstring() if has_unique_name else None
record = (
count,
properties,
underlying_type,
fields,
name,
)
case "LF_BITFIELD":
underlying_type = self.u32()
length = self.u8()
position = self.u8()
record = (underlying_type, length, position)
case _:
record = ()
raise ValueError(
f"Record {hex(record_type)} / {self.REC_TYPES.get(record_type, '???')} : not implemented"
)
return self.REC_TYPES[record_type], record
import io
class Msf(object):
def __init__(self, path=None, content=None):
if content is not None:
self.f = f = io.BytesIO(content)
else:
with open(path, "rb") as f_ondisk:
self.f = f = io.BytesIO(f_ondisk.read())
FileMagic = f.read(32)
assert FileMagic == b"Microsoft C/C++ MSF 7.00\r\n" + bytes.fromhex("1A 44 53 00 00 00")
self.BlockSize = blockSize = u32f(f)
self.FreeBlockMapBlock = u32f(f)
self.NumBlocks = u32f(f)
self.NumDirectoryBytes = u32f(f)
self.Unknown = u32f(f)
self.BlockMapAddr = u32f(f)
self.StreamDirectory = MsfStreamDirectory(self)
def __del__(self):
self.f.close()
@cache
def Stream(self, stream_number):
return MsfStream(
self,
self.StreamDirectory.StreamSize(stream_number),
self.StreamDirectory.StreamBlocks(stream_number),
)
class Pdb(Msf):
@cached_property
def PDBStream(self):
return PdbInfoStream(
self,
self.StreamDirectory.StreamSize(1),
self.StreamDirectory.StreamBlocks(1),
)
@cached_property
def DBIStream(self):
return DBIStream(
self,
self.StreamDirectory.StreamSize(3),
self.StreamDirectory.StreamBlocks(3),
)
@cached_property
def TPIStream(self):
return TPIorIPStream(
self,
self.StreamDirectory.StreamSize(2),
self.StreamDirectory.StreamBlocks(2),
)
@cached_property
def IPIStream(self):
return TPIorIPStream(
self,
self.StreamDirectory.StreamSize(4),
self.StreamDirectory.StreamBlocks(4),
)
def get_field_offset(self, structname, fieldname):
tpistream = self.TPIStream
if not tpistream.types_parsed:
save_filter = tpistream.filter
tpistream.filter = [
tpistream.REC_TYPES_ids["LF_FIELDLIST"],
tpistream.REC_TYPES_ids["LF_STRUCTURE"],
]
for _ in tpistream:
pass
tpistream.filter = save_filter
structname = structname.encode()
for struct_id, t in tpistream.types.items():
if t[0] == "LF_STRUCTURE":
if t[1][2] != 0 and t[1][6] == structname:
break
else:
raise ValueError(f"Structure {structname} not found in PDB")
fieldlist_id = t[1][2]
fieldlist = tpistream.types[fieldlist_id][1][0]
fieldname = fieldname.encode()
for field in fieldlist:
if fieldname == field[1][3]:
break
else:
raise ValueError(f"Field {fieldname} not found in structure {structname}")
field_offset = field[1][2]
return field_offset
def get_symbol_offset(self, symbol: str) -> int:
offset, segment = self.DBIStream.SymRecordStream.search_and_cache_symbols(symbol)
if offset == segment == None:
return None
section_virtual_address = self.DBIStream.SectionHeadersStream[segment - 1].VirtualAddress
return section_virtual_address + offset
+38 -23
View File
@@ -12,6 +12,9 @@ static const std::wstring attacks_provider = L"{72248466-7166-4feb-a386-34d8f35b
static const std::wstring hooks_provider = L"{72248411-7166-4feb-a386-34d8f35bb637}"; // Hooks
bool minimal = false;
bool is_admin() {
BOOL is_admin = FALSE;
PSID admin_group = nullptr;
@@ -68,26 +71,29 @@ int wmain(int argc, wchar_t* argv[]) {
return 1;
}
std::wstring provider_guid_str;
if (argc == 2) {
wchar_t *p = argv[1];
if (lstrcmpW(p, L"EDRi") == 0) {
provider_guid_str = edri_provider;
}
else if (lstrcmpW(p, L"Attack") == 0) {
provider_guid_str = attacks_provider;
}
else if (lstrcmpW(p, L"Hooks") == 0) {
provider_guid_str = hooks_provider;
}
else {
std::wcerr << L"[!] Usage: " << argv[0] << L" [EDRi | Attack | Hooks]\n";
return 1;
}
if (argc < 2 || argc > 3) {
std::wcerr << L"[!] Usage: " << argv[0] << L" [EDRi | Attack | Hooks] (minimal)\n";
return 1;
}
if (argc == 3) {
minimal = true;
}
// argc == 2
wchar_t *p = argv[1];
if (lstrcmpW(p, L"EDRi") == 0) {
provider_guid_str = edri_provider;
}
else if (lstrcmpW(p, L"Attack") == 0) {
provider_guid_str = attacks_provider;
}
else if (lstrcmpW(p, L"Hooks") == 0) {
provider_guid_str = hooks_provider;
}
else {
std::wcerr << L"[!] Usage: " << argv[0] << L" [EDRi | Attack | Hooks]\n";
return 1;
}
try {
// Create provider
krabs::guid provider_guid(provider_guid_str);
@@ -127,21 +133,30 @@ int wmain(int argc, wchar_t* argv[]) {
std::wstring wmsg = char2wstring(msg);
std::wstring wtime = char2wstring(iso_time.c_str());
std::wcout << L"PID: " << record.EventHeader.ProcessId
<< L" : " << wtask << L"\n";
std::wcout << L" message: " << wmsg << L"\n";
std::wcout << L" timestamp: " << wtime << L"\n";
if (targetpid != static_cast<uint64_t>(-1)) {
std::wcout << L" targetpid: " << targetpid << L"\n";
if (minimal) {
std::wcout << wtime << L": ";
if (targetpid != static_cast<uint64_t>(-1)) {
std::wcout << L"TargetPID=" << std::left << std::setw(5) << std::setfill(L' ') << targetpid << L" - ";
}
std::wcout << wmsg << L"\n";
}
else {
std::wcout << L"PID: " << record.EventHeader.ProcessId
<< L" : " << wtask << L"\n";
std::wcout << L" message: " << wmsg << L"\n";
std::wcout << L" timestamp: " << wtime << L"\n";
if (targetpid != static_cast<uint64_t>(-1)) {
std::wcout << L" targetpid: " << targetpid << L"\n";
}
std::wcout << L"--------------------------\n";
}
std::wcout << L"--------------------------\n";
});
// Trace session
krabs::user_trace trace(L"SimpleKrabsTrace");
trace.enable(provider);
std::wcout << L"[*] Listening to " << provider_guid_str << L"... Press Ctrl+C to exit" << L"\n";
std::wcout << L"[*] Listening to " << provider_guid_str << L"..." << L"\n";
trace.start(); // blocks
}
catch (const std::exception& e) {
Binary file not shown.
Binary file not shown.