Adding module sniping helpers for parsing exports and assessing blast radius

This commit is contained in:
toneillcodes
2026-06-02 12:28:26 -04:00
parent aaad5cad95
commit 25576abde4
2 changed files with 204 additions and 0 deletions
+112
View File
@@ -0,0 +1,112 @@
import os
import argparse
import pefile
def auto_int(x):
"""Helper to accept both decimal integers and hex strings (0x...) from CLI."""
return int(x, 0)
def analyze_stomp_blast_radius(dll_path, target_function, payload_size):
"""
Calculates the contiguous window available for a target function and maps
out exactly how many downstream exports will be overwritten by a given payload size.
"""
target_function_lower = target_function.lower()
if not os.path.exists(dll_path):
print(f"[-] Error: File not found at '{dll_path}'")
return
try:
pe = pefile.PE(dll_path)
if not hasattr(pe, 'DIRECTORY_ENTRY_EXPORT'):
print(f"[-] Error: '{os.path.basename(dll_path)}' has no Export Address Table.")
return
# Gather and map all exports
exports = []
target_rva = None
for exp in pe.DIRECTORY_ENTRY_EXPORT.symbols:
if exp.address is None:
continue
name = exp.name.decode('utf-8', errors='ignore') if exp.name else f"Ordinal_{exp.ordinal}"
exports.append({'name': name, 'rva': exp.address})
if name.lower() == target_function_lower:
target_rva = exp.address
if target_rva is None:
print(f"[-] Error: Function '{target_function}' not found in {os.path.basename(dll_path)}.")
return
# Sort the exports sequentially by their memory layout
exports_sorted = sorted(exports, key=lambda x: x['rva'])
# Calculate the payload's absolute ceiling in memory
stomp_ceiling_rva = target_rva + payload_size
# Find our target's position index
target_index = next(i for i, exp in enumerate(exports_sorted) if exp['rva'] == target_rva)
# Calculate single function maximum continuous space
available_isolated_space = 0
if target_index + 1 < len(exports_sorted):
available_isolated_space = exports_sorted[target_index + 1]['rva'] - target_rva
else:
# Fallback to section boundary if it's the final export
for section in pe.sections:
if section.VirtualAddress <= target_rva < (section.VirtualAddress + section.Misc_VirtualSize):
available_isolated_space = (section.VirtualAddress + section.Misc_VirtualSize) - target_rva
break
# Analyze the blast radius (how many functions we intersect with)
overwritten_functions = []
for i in range(target_index + 1, len(exports_sorted)):
next_exp = exports_sorted[i]
# If the next function starts before our payload ends, it's caught in the blast radius
if next_exp['rva'] < stomp_ceiling_rva:
overwritten_functions.append(next_exp)
else:
break # We outgrew the payload boundary
# Format Reporting Output
print("\n" + "="*90)
print(f"💥 MODULE STOMP BLAST RADIUS ANALYSIS: {os.path.basename(dll_path)}")
print("="*90)
print(f"Target Function : {target_function}")
print(f"Target Function RVA : {hex(target_rva)}")
print(f"Your Payload Size : {payload_size} bytes ({hex(payload_size)})")
print(f"Isolated Exec Space : {available_isolated_space} bytes ({hex(available_isolated_space)})")
print("-"*90)
if payload_size <= available_isolated_space:
print("🟢 STATUS: [SAFE FIT]")
print(f"Your payload fits entirely within the isolated boundary of {target_function}.")
print("Zero adjacent exported functions will be modified.")
else:
print("🔴 STATUS: [OVERFLOW RISK / COLLATERAL DAMAGE]")
print(f"Your payload overflows the isolated function boundary by {payload_size - available_isolated_space} bytes.")
print(f"Total adjacent exports completely or partially overwritten: {len(overwritten_functions)}")
print("\n🚨 Overwritten Functions List:")
print(f" {'Offset From Target':<22} | {'Function Name':<45} | {'Start RVA':<15}")
print(" " + "-"*85)
for exp in overwritten_functions:
offset = exp['rva'] - target_rva
print(f" +{offset:<20} | {exp['name']:<45} | {hex(exp['rva'])}")
print("="*90 + "\n")
except Exception as e:
print(f"[-] An error occurred: {e}")
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Analyze the collateral damage/blast radius of a specific stomp payload.")
parser.add_argument("-f", "--file", type=str, required = True, help="Path to the target DLL.")
parser.add_argument("-fnc", "--function", type=str, required=True, help="The export function to start stomping from.")
parser.add_argument("-s", "--size", type=auto_int, required=True, help="Size of your payload in bytes (can be int or hex string like 0x200).")
args = parser.parse_args()
analyze_stomp_blast_radius(args.file, args.function, args.size)
+92
View File
@@ -0,0 +1,92 @@
import os
import argparse
import pefile
def dump_module_exports(dll_path, output_file=None, names_only=False):
"""
Parses a local PE file, extracts the Export Address Table (EAT),
and displays a structured list of names, ordinals, and RVAs sorted by layout.
Optionally outputs a raw list of function names only and saves to a file.
"""
if not os.path.exists(dll_path):
print(f"[-] Error: Target file not found at '{dll_path}'")
return
try:
# Load the PE file and explicitly force parsing of the export directory
pe = pefile.PE(dll_path, fast_load=False)
if not hasattr(pe, 'DIRECTORY_ENTRY_EXPORT'):
print(f"[-] Error: '{os.path.basename(dll_path)}' has no Export Address Table (EAT).")
return
exported_functions = []
# Iterate over the parsed symbols in the export directory
for exp in pe.DIRECTORY_ENTRY_EXPORT.symbols:
# Derive name or fallback to ordinal string representation if un-exported by name
if exp.name:
func_name = exp.name.decode('utf-8', errors='ignore')
else:
func_name = f"[No Symbol] Ordinal_{exp.ordinal}"
exported_functions.append({
'name': func_name,
'ordinal': exp.ordinal,
'rva': exp.address if exp.address else 0
})
# Sort functions sequentially by RVA so it maps linearly to memory layout
exported_functions_sorted = sorted(exported_functions, key=lambda x: x['rva'])
lines = []
if names_only:
# Generate a raw list of names only (filtering out un-named functions if desired)
for func in exported_functions_sorted:
# Omit default placeholder strings if you want clean include/exclude lists
if not func['name'].startswith("[No Symbol]"):
lines.append(func['name'])
else:
# Generate full structured diagnostic manifest
lines.append("\n" + "="*95)
lines.append(f"📦 EXPORT ADDRESS TABLE MANIFEST: {os.path.basename(dll_path)}")
lines.append("="*95)
lines.append(f"{'Exported Symbol / Function Name':<55} | {'Ordinal':<10} | {'Relative Virtual Address (RVA)':<15}")
lines.append("-"*95)
for func in exported_functions_sorted:
rva_str = hex(func['rva']) if func['rva'] != 0 else "Forwarded / N/A"
lines.append(f"{func['name']:<55} | {func['ordinal']:<10} | {rva_str:<15}")
lines.append("-"*95)
lines.append(f"[+] Successfully extracted {len(exported_functions_sorted)} exports from the EAT.")
lines.append("="*95 + "\n")
# Output to screen
for line in lines:
print(line)
# Write to file if specified
if output_file:
try:
with open(output_file, 'w', encoding='utf-8') as f:
for line in lines:
f.write(line + '\n')
print(f"[+] Successfully saved output to: '{output_file}'\n")
except Exception as file_err:
print(f"[-] Warning: Failed to write output to file: {file_err}")
except pefile.PEFormatError:
print(f"[-] Error: '{dll_path}' is not a valid Portable Executable file.")
except Exception as e:
print(f"[-] An unexpected processing error occurred: {e}")
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Extract and map the complete Export Address Table layout from a target PE module.")
parser.add_argument("-f", "--file", type=str, required=True, help="Path to the target DLL on disk.")
parser.add_argument("-o", "--output", type=str, default=None, help="Optional path to a text file to write the output to.")
parser.add_argument("--names-only", action="store_true", help="Output a clean list of function names only, omitting headers, RVAs, and ordinals.")
args = parser.parse_args()
dump_module_exports(args.file, args.output, args.names_only)