mirror of
https://github.com/evilele/EDR-Introspection
synced 2026-06-09 08:11:09 +00:00
rework demos
This commit is contained in:
+4
-1
@@ -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/
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
@@ -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
@@ -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.
Reference in New Issue
Block a user