Move all executable handling to Executable

This commit is contained in:
Matt Nadareski
2024-11-05 01:12:18 -05:00
parent eb8b9daea8
commit f04cf25fa9
3 changed files with 217 additions and 292 deletions

View File

@@ -1,9 +1,11 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using BinaryObjectScanner.Data;
using BinaryObjectScanner.Interfaces;
using SabreTools.IO.Extensions;
using SabreTools.Serialization.Interfaces;
using SabreTools.Serialization.Wrappers;
namespace BinaryObjectScanner.FileType
@@ -44,43 +46,94 @@ namespace BinaryObjectScanner.FileType
/// <inheritdoc/>
public string? Detect(Stream stream, string file, bool includeDebug)
{
// Try to create a wrapper for the proper executable type
var wrapper = WrapperFactory.CreateExecutableWrapper(stream);
if (wrapper == null)
// Get all non-nested protections
var protections = DetectDict(stream, file, scanner: null, includeDebug);
if (protections.Count == 0)
return null;
// Create the internal list
var protections = new List<string>();
var protectionList = new List<string>();
foreach (string key in protections.Keys)
{
protectionList.AddRange(protections[key]);
}
return string.Join(";", [.. protections]);
}
/// <inheritdoc cref="IDetectable.Detect(Stream, string, bool)"/>
/// <remarks>
/// Ideally, we wouldn't need to circumvent the proper handling of file types just for Executable,
/// but due to the complexity of scanning, this is not currently possible.
/// </remarks>
public ProtectionDictionary DetectDict(Stream stream, string file, Scanner? scanner, bool includeDebug)
{
// Create the output dictionary
var protections = new ProtectionDictionary();
// Try to create a wrapper for the proper executable type
IWrapper? wrapper;
try
{
wrapper = WrapperFactory.CreateExecutableWrapper(stream);
if (wrapper == null)
return protections;
}
catch (Exception ex)
{
if (includeDebug) Console.WriteLine(ex);
return protections;
}
// Only use generic content checks if we're in debug mode
if (includeDebug)
{
var contentProtections = RunContentChecks(file, stream, includeDebug);
protections.AddRange(contentProtections.Values);
var subProtections = RunContentChecks(file, stream, includeDebug);
protections.Append(file, subProtections.Values);
}
if (wrapper is MSDOS mz)
{
// Standard checks
var subProtections = RunExecutableChecks(file, mz, StaticChecks.MSDOSExecutableCheckClasses, includeDebug);
protections.AddRange(subProtections.Values);
protections.Append(file, subProtections.Values);
// Extractable checks
var extractedProtections = HandleExtractableProtections(file, mz, subProtections.Keys, scanner, includeDebug);
protections.Append(extractedProtections);
}
else if (wrapper is LinearExecutable lex)
{
// Standard checks
var subProtections = RunExecutableChecks(file, lex, StaticChecks.LinearExecutableCheckClasses, includeDebug);
protections.AddRange(subProtections.Values);
protections.Append(file, subProtections.Values);
// Extractable checks
var extractedProtections = HandleExtractableProtections(file, lex, subProtections.Keys, scanner, includeDebug);
protections.Append(extractedProtections);
}
else if (wrapper is NewExecutable nex)
{
// Standard checks
var subProtections = RunExecutableChecks(file, nex, StaticChecks.NewExecutableCheckClasses, includeDebug);
protections.AddRange(subProtections.Values);
protections.Append(file, subProtections.Values);
// Extractable checks
var extractedProtections = HandleExtractableProtections(file, nex, subProtections.Keys, scanner, includeDebug);
protections.Append(extractedProtections);
}
else if (wrapper is PortableExecutable pex)
{
// Standard checks
var subProtections = RunExecutableChecks(file, pex, StaticChecks.PortableExecutableCheckClasses, includeDebug);
protections.AddRange(subProtections.Values);
protections.Append(file, subProtections.Values);
// Extractable checks
var extractedProtections = HandleExtractableProtections(file, pex, subProtections.Keys, scanner, includeDebug);
protections.Append(extractedProtections);
}
return string.Join(";", [.. protections]);
return protections;
}
#region Check Runners
@@ -145,6 +198,7 @@ namespace BinaryObjectScanner.FileType
/// <param name="file">Name of the source file of the executable, for tracking</param>
/// <param name="exe">Executable to scan</param>
/// <param name="checks">Set of checks to use</param>
/// <param name="scanner">Scanner for handling recursive protections</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns>Set of protections in file, empty on error</returns>
public IDictionary<U, string> RunExecutableChecks<T, U>(string file, T exe, List<U> checks, bool includeDebug)
@@ -176,6 +230,74 @@ namespace BinaryObjectScanner.FileType
return protections;
}
/// <summary>
/// Handle extractable protections, such as executable packers
/// </summary>
/// <param name="file">Name of the source file of the stream, for tracking</param>
/// <param name="exe">Executable to scan the contents of</param>
/// <param name="checks">Set of classes returned from Exectuable scans</param>
/// <param name="scanner">Scanner for handling recursive protections</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns>Set of protections found from extraction, empty on error</returns>
private ProtectionDictionary HandleExtractableProtections<T, U>(string file, T exe, IEnumerable<U> checks, Scanner? scanner, bool includeDebug)
where T : WrapperBase
where U : IExecutableCheck<T>
{
// Create the output dictionary
var protections = new ProtectionDictionary();
// If we have an invalid set of classes
if (checks == null || !checks.Any())
return protections;
// If we have any extractable packers
var extractables = checks
.Where(c => c is IExtractableExecutable<T>)
.Select(c => c as IExtractableExecutable<T>);
extractables.IterateWithAction(extractable =>
{
// If we have an invalid extractable somehow
if (extractable == null)
return;
// If the extractable file itself fails
try
{
// Extract and get the output path
string tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
bool extracted = extractable.Extract(file, exe, tempPath, includeDebug);
// Collect and format all found protections
ProtectionDictionary? subProtections = null;
if (extracted)
subProtections = scanner?.GetProtections(tempPath);
// If temp directory cleanup fails
try
{
if (Directory.Exists(tempPath))
Directory.Delete(tempPath, true);
}
catch (Exception ex)
{
if (includeDebug) Console.WriteLine(ex);
}
// Prepare the returned protections
subProtections?.StripFromKeys(tempPath);
subProtections?.PrependToKeys(file);
if (subProtections != null)
protections.Append(subProtections);
}
catch (Exception ex)
{
if (includeDebug) Console.WriteLine(ex);
}
});
return protections;
}
#endregion
#region Helpers

View File

@@ -1,126 +0,0 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using BinaryObjectScanner.Data;
using BinaryObjectScanner.Interfaces;
namespace BinaryObjectScanner
{
internal static class Handler
{
#region Multiple Implementation Wrappers
/// <summary>
/// Handle a single path based on all path check implementations
/// </summary>
/// <param name="path">Path of the file or directory to check</param>
/// <param name="scanner">Scanner object to use for options and scanning</param>
/// <returns>Set of protections in file, null on error</returns>
public static ProtectionDictionary HandlePathChecks(string path, IEnumerable<string>? files)
{
// Create the output dictionary
var protections = new ProtectionDictionary();
// Preprocess the list of files
files = files?
.Select(f => f.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar))?
.ToList();
// Iterate through all checks
StaticChecks.PathCheckClasses.IterateWithAction(checkClass =>
{
var subProtections = checkClass.PerformCheck(path, files);
protections.Append(path, subProtections);
});
return protections;
}
#endregion
#region Single Implementation Handlers
/// <summary>
/// Handle files based on an IDetectable implementation
/// </summary>
/// <param name="impl">IDetectable class representing the file type</param>
/// <param name="fileName">Name of the source file of the stream, for tracking</param>
/// <param name="stream">Stream to scan the contents of</param>
/// <param name="includeDebug">True to include debug data, false otherwise</param>
/// <returns>Set of protections in file, null on error</returns>
public static List<string>? HandleDetectable(IDetectable impl, string fileName, Stream stream, bool includeDebug)
{
var protection = impl.Detect(stream, fileName, includeDebug);
return ProcessProtectionString(protection);
}
/// <summary>
/// Handle files based on an IPathCheck implementation
/// </summary>
/// <param name="impl">IPathCheck class representing the file type</param>
/// <param name="path">Path of the file or directory to check</param>
/// <returns>Set of protections in path, empty on error</returns>
private static List<string> PerformCheck(this IPathCheck impl, string? path, IEnumerable<string>? files)
{
// If we have an invalid path
if (string.IsNullOrEmpty(path))
return [];
// Setup the list
var protections = new List<string>();
// If we have a file path
if (File.Exists(path))
{
var protection = impl.CheckFilePath(path!);
var subProtections = ProcessProtectionString(protection);
if (subProtections != null)
protections.AddRange(subProtections);
}
// If we have a directory path
if (Directory.Exists(path) && files?.Any() == true)
{
var subProtections = impl.CheckDirectoryPath(path!, files);
if (subProtections != null)
protections.AddRange(subProtections);
}
return protections;
}
#endregion
#region Helpers
/// <summary>
/// Process a protection string if it includes multiple protections
/// </summary>
/// <param name="protection">Protection string to process</param>
/// <returns>Set of protections parsed, null on error</returns>
private static List<string>? ProcessProtectionString(string? protection)
{
// If we have an invalid protection string
if (string.IsNullOrEmpty(protection))
return null;
// Setup the output queue
var protections = new List<string>();
// If we have an indicator of multiple protections
if (protection!.Contains(";"))
{
var splitProtections = protection.Split(';');
protections.AddRange(splitProtections);
}
else
{
protections.Add(protection);
}
return protections;
}
#endregion
}
}

View File

@@ -6,7 +6,6 @@ using BinaryObjectScanner.Data;
using BinaryObjectScanner.FileType;
using BinaryObjectScanner.Interfaces;
using SabreTools.IO.Extensions;
using SabreTools.Serialization.Interfaces;
using SabreTools.Serialization.Wrappers;
namespace BinaryObjectScanner
@@ -70,18 +69,18 @@ namespace BinaryObjectScanner
/// </summary>
/// <param name="path">Path to scan</param>
/// <returns>Dictionary of list of strings representing the found protections</returns>
public ProtectionDictionary? GetProtections(string path)
public ProtectionDictionary GetProtections(string path)
=> GetProtections([path]);
/// <summary>
/// Scan the list of paths and get all found protections
/// </summary>
/// <returns>Dictionary of list of strings representing the found protections</returns>
public ProtectionDictionary? GetProtections(List<string>? paths)
public ProtectionDictionary GetProtections(List<string>? paths)
{
// If we have no paths, we can't scan
if (paths == null || !paths.Any())
return null;
return [];
// Set a starting starting time for debug output
DateTime startTime = DateTime.UtcNow;
@@ -106,7 +105,7 @@ namespace BinaryObjectScanner
// Scan for path-detectable protections
if (_options.ScanPaths)
{
var directoryPathProtections = Handler.HandlePathChecks(path, files);
var directoryPathProtections = HandlePathChecks(path, files);
protections.Append(directoryPathProtections);
}
@@ -127,7 +126,7 @@ namespace BinaryObjectScanner
// Scan for path-detectable protections
if (_options.ScanPaths)
{
var filePathProtections = Handler.HandlePathChecks(file, files: null);
var filePathProtections = HandlePathChecks(file, files: null);
if (filePathProtections != null && filePathProtections.Any())
protections.Append(filePathProtections);
}
@@ -158,7 +157,7 @@ namespace BinaryObjectScanner
// Scan for path-detectable protections
if (_options.ScanPaths)
{
var filePathProtections = Handler.HandlePathChecks(path, files: null);
var filePathProtections = HandlePathChecks(path, files: null);
if (filePathProtections != null && filePathProtections.Any())
protections.Append(filePathProtections);
}
@@ -197,11 +196,11 @@ namespace BinaryObjectScanner
/// </summary>
/// <param name="file">Path to the file to scan</param>
/// <returns>Dictionary of list of strings representing the found protections</returns>
private ProtectionDictionary? GetInternalProtections(string file)
private ProtectionDictionary GetInternalProtections(string file)
{
// Quick sanity check before continuing
if (!File.Exists(file))
return null;
return [];
// Open the file and begin scanning
try
@@ -226,11 +225,11 @@ namespace BinaryObjectScanner
/// <param name="fileName">Name of the source file of the stream, for tracking</param>
/// <param name="stream">Stream to scan the contents of</param>
/// <returns>Dictionary of list of strings representing the found protections</returns>
private ProtectionDictionary? GetInternalProtections(string fileName, Stream stream)
private ProtectionDictionary GetInternalProtections(string fileName, Stream stream)
{
// Quick sanity check before continuing
if (stream == null || !stream.CanRead || !stream.CanSeek)
return null;
return [];
// Initialize the protections found
var protections = new ProtectionDictionary();
@@ -252,13 +251,13 @@ namespace BinaryObjectScanner
{
if (_options.IncludeDebug) Console.WriteLine(ex);
return null;
return [];
}
// Get the file type either from magic number or extension
WrapperType fileType = WrapperFactory.GetFileType(magic, extension);
if (fileType == WrapperType.UNKNOWN)
return null;
return [];
#region Non-Archive File Types
@@ -273,32 +272,16 @@ namespace BinaryObjectScanner
{
executable.IncludeGameEngines = _options.ScanGameEngines;
executable.IncludePackers = _options.ScanPackers;
var subProtections = ProcessExecutable(executable, fileName, stream);
if (subProtections != null)
protections.Append(subProtections);
var subProtections = executable.DetectDict(stream, fileName, this, _options.IncludeDebug);
protections.Append(subProtections);
}
// Otherwise, use the default implementation
else
{
var subProtections = Handler.HandleDetectable(detectable, fileName, stream, _options.IncludeDebug);
if (subProtections != null)
protections.Append(fileName, subProtections);
}
var subProtection = detectable.Detect(stream, fileName, _options.IncludeDebug);
if (!string.IsNullOrEmpty(subProtection))
{
// If we have an indicator of multiple protections
if (subProtection.Contains(';'))
{
var splitProtections = subProtection!.Split(';');
protections.Append(fileName, splitProtections);
}
else
{
protections.Append(fileName, subProtection!);
}
var subProtection = detectable.Detect(stream, fileName, _options.IncludeDebug);
protections.Append(fileName, ProcessProtectionString(subProtection));
}
}
@@ -363,150 +346,96 @@ namespace BinaryObjectScanner
#endregion
#region Executable Handling
#region Path Handling
/// <summary>
/// Process scanning for an Executable type
/// Handle a single path based on all path check implementations
/// </summary>
/// <param name="executable">Executable instance for processing</param>
/// <param name="fileName">Name of the source file of the stream, for tracking</param>
/// <param name="stream">Stream to scan the contents of</param>
/// <remarks>
/// Ideally, we wouldn't need to circumvent the proper handling of file types just for Executable,
/// but due to the complexity of scanning, this is not currently possible.
/// </remarks>
private ProtectionDictionary? ProcessExecutable(Executable executable, string fileName, Stream stream)
/// <param name="path">Path of the file or directory to check</param>
/// <param name="scanner">Scanner object to use for options and scanning</param>
/// <returns>Set of protections in file, null on error</returns>
private static ProtectionDictionary HandlePathChecks(string path, IEnumerable<string>? files)
{
// Try to create a wrapper for the proper executable type
IWrapper? wrapper;
try
{
wrapper = WrapperFactory.CreateExecutableWrapper(stream);
if (wrapper == null)
return null;
}
catch (Exception ex)
{
if (_options.IncludeDebug) Console.WriteLine(ex);
return null;
}
// Create the output dictionary
var protections = new ProtectionDictionary();
// Only use generic content checks if we're in debug mode
if (_options.IncludeDebug)
{
var subProtections = executable.RunContentChecks(fileName, stream, _options.IncludeDebug);
protections.Append(fileName, subProtections.Values);
}
// Preprocess the list of files
files = files?
.Select(f => f.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar))?
.ToList();
if (wrapper is MSDOS mz)
// Iterate through all checks
StaticChecks.PathCheckClasses.IterateWithAction(checkClass =>
{
// Standard checks
var subProtections = executable.RunExecutableChecks(fileName, mz, StaticChecks.MSDOSExecutableCheckClasses, _options.IncludeDebug);
protections.Append(fileName, subProtections.Values);
// Extractable checks
var extractedProtections = HandleExtractableProtections(fileName, mz, subProtections.Keys);
protections.Append(extractedProtections);
}
else if (wrapper is LinearExecutable lex)
{
// Standard checks
var subProtections = executable.RunExecutableChecks(fileName, lex, StaticChecks.LinearExecutableCheckClasses, _options.IncludeDebug);
protections.Append(fileName, subProtections.Values);
// Extractable checks
var extractedProtections = HandleExtractableProtections(fileName, lex, subProtections.Keys);
protections.Append(extractedProtections);
}
else if (wrapper is NewExecutable nex)
{
// Standard checks
var subProtections = executable.RunExecutableChecks(fileName, nex, StaticChecks.NewExecutableCheckClasses, _options.IncludeDebug);
protections.Append(fileName, subProtections.Values);
// Extractable checks
var extractedProtections = HandleExtractableProtections(fileName, nex, subProtections.Keys);
protections.Append(extractedProtections);
}
else if (wrapper is PortableExecutable pex)
{
// Standard checks
var subProtections = executable.RunExecutableChecks(fileName, pex, StaticChecks.PortableExecutableCheckClasses, _options.IncludeDebug);
protections.Append(fileName, subProtections.Values);
// Extractable checks
var extractedProtections = HandleExtractableProtections(fileName, pex, subProtections.Keys);
protections.Append(extractedProtections);
}
var subProtections = PerformCheck(checkClass, path, files);
protections.Append(path, subProtections);
});
return protections;
}
/// <summary>
/// Handle extractable protections, such as executable packers
/// Handle files based on an IPathCheck implementation
/// </summary>
/// <param name="file">Name of the source file of the stream, for tracking</param>
/// <param name="exe">Executable to scan the contents of</param>
/// <param name="checks">Set of classes returned from Exectuable scans</param>
/// <returns>Set of protections found from extraction, null on error</returns>
private ProtectionDictionary HandleExtractableProtections<T, U>(string file, T exe, IEnumerable<U> checks)
where T : WrapperBase
where U : IExecutableCheck<T>
/// <param name="impl">IPathCheck class representing the file type</param>
/// <param name="path">Path of the file or directory to check</param>
/// <returns>Set of protections in path, empty on error</returns>
private static List<string> PerformCheck(IPathCheck impl, string? path, IEnumerable<string>? files)
{
// Create the output dictionary
var protections = new ProtectionDictionary();
// If we have an invalid path
if (string.IsNullOrEmpty(path))
return [];
// If we have an invalid set of classes
if (checks == null || !checks.Any())
return protections;
// Setup the list
var protections = new List<string>();
// If we have any extractable packers
var extractables = checks
.Where(c => c is IExtractableExecutable<T>)
.Select(c => c as IExtractableExecutable<T>);
extractables.IterateWithAction(extractable =>
// If we have a file path
if (File.Exists(path))
{
// If we have an invalid extractable somehow
if (extractable == null)
return;
var protection = impl.CheckFilePath(path!);
var subProtections = ProcessProtectionString(protection);
protections.AddRange(subProtections);
}
// If the extractable file itself fails
try
{
// Extract and get the output path
string tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
bool extracted = extractable.Extract(file, exe, tempPath, _options.IncludeDebug);
// If we have a directory path
if (Directory.Exists(path) && files?.Any() == true)
{
var subProtections = impl.CheckDirectoryPath(path!, files);
if (subProtections != null)
protections.AddRange(subProtections);
}
// Collect and format all found protections
ProtectionDictionary? subProtections = null;
if (extracted)
subProtections = GetProtections(tempPath);
return protections;
}
// If temp directory cleanup fails
try
{
if (Directory.Exists(tempPath))
Directory.Delete(tempPath, true);
}
catch (Exception ex)
{
if (_options.IncludeDebug) Console.WriteLine(ex);
}
#endregion
// Prepare the returned protections
subProtections?.StripFromKeys(tempPath);
subProtections?.PrependToKeys(file);
if (subProtections != null)
protections.Append(subProtections);
}
catch (Exception ex)
{
if (_options.IncludeDebug) Console.WriteLine(ex);
}
});
#region Helpers
/// <summary>
/// Process a protection string if it includes multiple protections
/// </summary>
/// <param name="protection">Protection string to process</param>
/// <returns>Set of protections parsed, empty on error</returns>
internal static List<string> ProcessProtectionString(string? protection)
{
// If we have an invalid protection string
if (string.IsNullOrEmpty(protection))
return [];
// Setup the output queue
var protections = new List<string>();
// If we have an indicator of multiple protections
if (protection!.Contains(";"))
{
var splitProtections = protection.Split(';');
protections.AddRange(splitProtections);
}
else
{
protections.Add(protection);
}
return protections;
}