Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ public class ExperimentalFeature
internal const string PSNativeCommandErrorActionPreferenceFeatureName = "PSNativeCommandErrorActionPreference";
internal const string PSRemotingSSHTransportErrorHandling = "PSRemotingSSHTransportErrorHandling";
internal const string PSCleanBlockFeatureName = "PSCleanBlock";
internal const string PSAMSIMethodInvocationLogging = "PSAMSIMethodInvocationLogging";

#endregion

Expand Down Expand Up @@ -134,6 +135,9 @@ static ExperimentalFeature()
new ExperimentalFeature(
name: PSCleanBlockFeatureName,
description: "Add support of a 'Clean' block to functions and script cmdlets for easy resource cleanup"),
new ExperimentalFeature(
name: PSAMSIMethodInvocationLogging,
description: "Provides AMSI notification of .NET method invocations."),
};

EngineExperimentalFeatures = new ReadOnlyCollection<ExperimentalFeature>(engineFeatures);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -639,7 +639,9 @@ internal static class CachedReflectionInfo

internal static readonly MethodInfo ArgumentTransformationAttribute_Transform =
typeof(ArgumentTransformationAttribute).GetMethod(nameof(ArgumentTransformationAttribute.Transform), InstancePublicFlags);
// ReSharper restore InconsistentNaming

internal static readonly MethodInfo MemberInvocationLoggingOps_LogMemberInvocation =
typeof(MemberInvocationLoggingOps).GetMethod(nameof(MemberInvocationLoggingOps.LogMemberInvocation), StaticFlags);
}

internal static class ExpressionCache
Expand Down
17 changes: 17 additions & 0 deletions src/System.Management.Automation/engine/runtime/Binding/Binders.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6903,6 +6903,23 @@ internal static DynamicMetaObject InvokeDotNetMethod(
expr = Expression.Block(expr, ExpressionCache.AutomationNullConstant);
}

if (ExperimentalFeature.IsEnabled(ExperimentalFeature.PSAMSIMethodInvocationLogging))
{
// Expression block runs two expressions in order:
// - Log method invocation to AMSI Notifications (can throw PSSecurityException)
// - Invoke method
string targetName = methodInfo.ReflectedType?.FullName ?? string.Empty;
expr = Expression.Block(
Expression.Call(
CachedReflectionInfo.MemberInvocationLoggingOps_LogMemberInvocation,
Expression.Constant(targetName),
Expression.Constant(name),
Expression.NewArrayInit(
typeof(object),
args.Select(static e => e.Expression.Cast(typeof(object))))),
expr);
}

// If we're calling SteppablePipeline.{Begin|Process|End}, we don't want
// to wrap exceptions - this is very much a special case to help error
// propagation and ensure errors are attributed to the correct code (the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3546,4 +3546,69 @@ internal static object[] GetSlice(IList list, int startIndex)
return result;
}
}

internal static class MemberInvocationLoggingOps
{
private static readonly Lazy<bool> DumpLogAMSIContent = new Lazy<bool>(
() => {
object result = Environment.GetEnvironmentVariable("__PSDumpAMSILogContent");
if (result != null && LanguagePrimitives.TryConvertTo(result, out int value))
{
return value == 1;
}
return false;
}
);

internal static void LogMemberInvocation(string targetName, string name, object[] args)
{
try
{
var contentName = "PowerShellMemberInvocation";
var argsBuilder = new Text.StringBuilder();

for (int i = 0; i < args.Length; i++)
{
string value = args[i] is null ? "null" : args[i].ToString();

if (i > 0)
{
argsBuilder.Append(", ");
}

argsBuilder.Append($"<{value}>");
}

string content = $"<{targetName}>.{name}({argsBuilder})";

if (DumpLogAMSIContent.Value)
{
Console.WriteLine("\n=== Amsi notification report content ===");
Console.WriteLine(content);
}

var success = AmsiUtils.ReportContent(
name: contentName,
content: content);

if (DumpLogAMSIContent.Value)
{
Console.WriteLine($"=== Amsi notification report success: {success} ===");
}
}
catch (PSSecurityException)
{
// ReportContent() will throw PSSecurityException if AMSI detects malware, which
// must be propagated.
throw;
}
catch (Exception ex)
{
if (DumpLogAMSIContent.Value)
{
Console.WriteLine($"!!! Amsi notification report exception: {ex} !!!");
}
}
}
}
}
175 changes: 146 additions & 29 deletions src/System.Management.Automation/security/SecuritySupport.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1383,7 +1383,10 @@ internal static AmsiNativeMethods.AMSI_RESULT ScanContent(string content, string
#endif
}

internal static AmsiNativeMethods.AMSI_RESULT WinScanContent(string content, string sourceMetadata, bool warmUp)
internal static AmsiNativeMethods.AMSI_RESULT WinScanContent(
string content,
string sourceMetadata,
bool warmUp)
{
if (string.IsNullOrEmpty(sourceMetadata))
{
Expand Down Expand Up @@ -1414,33 +1417,9 @@ internal static AmsiNativeMethods.AMSI_RESULT WinScanContent(string content, str

try
{
int hr = 0;

// Initialize AntiMalware Scan Interface, if not already initialized.
// If we failed to initialize previously, just return the neutral result ("AMSI_RESULT_NOT_DETECTED")
if (s_amsiContext == IntPtr.Zero)
if (!CheckAmsiInit())
{
hr = Init();

if (!Utils.Succeeded(hr))
{
s_amsiInitFailed = true;
return AmsiNativeMethods.AMSI_RESULT.AMSI_RESULT_NOT_DETECTED;
}
}

// Initialize the session, if one isn't already started.
// If we failed to initialize previously, just return the neutral result ("AMSI_RESULT_NOT_DETECTED")
if (s_amsiSession == IntPtr.Zero)
{
hr = AmsiNativeMethods.AmsiOpenSession(s_amsiContext, ref s_amsiSession);
AmsiInitialized = true;

if (!Utils.Succeeded(hr))
{
s_amsiInitFailed = true;
return AmsiNativeMethods.AMSI_RESULT.AMSI_RESULT_NOT_DETECTED;
}
return AmsiNativeMethods.AMSI_RESULT.AMSI_RESULT_NOT_DETECTED;
}

if (warmUp)
Expand All @@ -1453,6 +1432,7 @@ internal static AmsiNativeMethods.AMSI_RESULT WinScanContent(string content, str
AmsiNativeMethods.AMSI_RESULT result = AmsiNativeMethods.AMSI_RESULT.AMSI_RESULT_CLEAN;

// Run AMSI content scan
int hr;
unsafe
{
fixed (char* buffer = content)
Expand Down Expand Up @@ -1484,6 +1464,123 @@ internal static AmsiNativeMethods.AMSI_RESULT WinScanContent(string content, str
}
}

/// <Summary>
/// Reports provided content to AMSI (Antimalware Scan Interface).
/// </Summary>
/// <param name="name">Name of content being reported.</param>
/// <param name="content">Content being reported.</param>
/// <returns>True if content was successfully reported.</returns>
internal static bool ReportContent(
string name,
string content)
{
#if UNIX
return false;
#else
return WinReportContent(name, content);
#endif
}

private static bool WinReportContent(
string name,
string content)
{
if (string.IsNullOrEmpty(name) ||
string.IsNullOrEmpty(content) ||
s_amsiInitFailed ||
s_amsiNotifyFailed)
{
return false;
}

lock (s_amsiLockObject)
{
if (s_amsiNotifyFailed)
{
return false;
}

try
{
if (!CheckAmsiInit())
{
return false;
}

int hr;
AmsiNativeMethods.AMSI_RESULT result = AmsiNativeMethods.AMSI_RESULT.AMSI_RESULT_NOT_DETECTED;
unsafe
{
fixed (char* buffer = content)
{
var buffPtr = new IntPtr(buffer);
hr = AmsiNativeMethods.AmsiNotifyOperation(
amsiContext: s_amsiContext,
buffer: buffPtr,
length: (uint)(content.Length * sizeof(char)),
contentName: name,
ref result);
}
}

if (Utils.Succeeded(hr))
{
if (result == AmsiNativeMethods.AMSI_RESULT.AMSI_RESULT_DETECTED)
{
// If malware is detected, throw to prevent method invoke expression from running.
throw new PSSecurityException(ParserStrings.ScriptContainedMaliciousContent);
}

return true;
}

return false;
}
catch (DllNotFoundException)
{
s_amsiNotifyFailed = true;
return false;
}
catch (System.EntryPointNotFoundException)
{
s_amsiNotifyFailed = true;
return false;
}
}
}

private static bool CheckAmsiInit()
{
// Initialize AntiMalware Scan Interface, if not already initialized.
// If we failed to initialize previously, just return the neutral result ("AMSI_RESULT_NOT_DETECTED")
if (s_amsiContext == IntPtr.Zero)
{
int hr = Init();

if (!Utils.Succeeded(hr))
{
s_amsiInitFailed = true;
return false;
}
}

// Initialize the session, if one isn't already started.
// If we failed to initialize previously, just return the neutral result ("AMSI_RESULT_NOT_DETECTED")
if (s_amsiSession == IntPtr.Zero)
{
int hr = AmsiNativeMethods.AmsiOpenSession(s_amsiContext, ref s_amsiSession);
AmsiInitialized = true;

if (!Utils.Succeeded(hr))
{
s_amsiInitFailed = true;
return false;
}
}

return true;
}

internal static void CurrentDomain_ProcessExit(object sender, EventArgs e)
{
if (AmsiInitialized && !AmsiUninitializeCalled)
Expand All @@ -1499,6 +1596,7 @@ internal static void CurrentDomain_ProcessExit(object sender, EventArgs e)
private static IntPtr s_amsiSession = IntPtr.Zero;

private static bool s_amsiInitFailed = false;
private static bool s_amsiNotifyFailed = false;
private static readonly object s_amsiLockObject = new object();

/// <summary>
Expand Down Expand Up @@ -1623,8 +1721,27 @@ internal static extern int AmsiInitialize(
[DefaultDllImportSearchPathsAttribute(DllImportSearchPath.System32)]
[DllImportAttribute("amsi.dll", EntryPoint = "AmsiScanBuffer", CallingConvention = CallingConvention.StdCall)]
internal static extern int AmsiScanBuffer(
System.IntPtr amsiContext, System.IntPtr buffer, uint length,
[InAttribute()][MarshalAsAttribute(UnmanagedType.LPWStr)] string contentName, System.IntPtr amsiSession, ref AMSI_RESULT result);
System.IntPtr amsiContext,
System.IntPtr buffer,
uint length,
[InAttribute()][MarshalAsAttribute(UnmanagedType.LPWStr)] string contentName,
System.IntPtr amsiSession,
ref AMSI_RESULT result);

/// Return Type: HRESULT->LONG->int
/// amsiContext: HAMSICONTEXT->HAMSICONTEXT__*
/// buffer: PVOID->void*
/// length: ULONG->unsigned int
/// contentName: LPCWSTR->WCHAR*
/// result: AMSI_RESULT*
[DefaultDllImportSearchPathsAttribute(DllImportSearchPath.System32)]
[DllImportAttribute("amsi.dll", EntryPoint = "AmsiNotifyOperation", CallingConvention = CallingConvention.StdCall)]
internal static extern int AmsiNotifyOperation(
System.IntPtr amsiContext,
System.IntPtr buffer,
uint length,
[InAttribute()][MarshalAsAttribute(UnmanagedType.LPWStr)] string contentName,
ref AMSI_RESULT result);

/// Return Type: HRESULT->LONG->int
///amsiContext: HAMSICONTEXT->HAMSICONTEXT__*
Expand Down