[C#] 呼叫 PowerShell 取得 Windows Event 4624 的 JSON 資料

2023-06-01

最近,因為一些事情我需要 去檢測關於 Windows Event 4624 的事件 ,剛好 黑暗執行緒的一篇文章,我就想說

可以透過這方法來做到,不過黑大是使用 PowerShell ,這東西對我來說是一個完全完全陌生的領域,不過我就想黑大都分享了

那我可以使用黑大的分享,然後我透過程式端去抓就可以達到我的需求,果然是一個及時雨



中間遇到一些問題,我就在文章記錄一下,這範例我會去撈取過去 12 小時內 4624 也就是 Windows 被登入的  event 代碼,主要透過 C# 去呼叫黑大寫的 PS 

不過這中間我修改過,我將他轉成 JSON,這樣我在程式端會比較好處理,這裡面我就記錄一些比較麻煩的地方

1. 顯示中文問題,基本上我是基於 這篇文章改中文問,不然會一直出現亂碼

我在 PowerShell 下 $PROFILE 得到一個指定讀取的路徑並加在該ps 中寫

$OutputEncoding = [console]::InputEncoding = [console]::OutputEncoding = [Text.UTF8Encoding]::UTF8

然後,主要 run 的 ps1 主要存檔時選擇具有 BOM 的 UTF8 


這樣中文就正常了


2. 我主要思維不想自己去寫撈取 Windows Event 的東西,所以我的思維就是直接呼叫 黑大的 ps code ,然後得到答案後不就完美所以我參考了了這篇程式碼

https://gist.github.com/SQLJana/1738dff1b151e2412240067167076e7a

但是我拿掉一些我不需要的部分,感謝開源前輩的貢獻

//origion from https://gist.github.com/SQLJana/1738dff1b151e2412240067167076e7a using System; using System.Diagnostics; using System.Collections.ObjectModel; //Used for CollectionSystem.Collections.ObjectModel.Collection using System.ComponentModel; using System.Data; using System.Management.Automation.Runspaces; using System.Management.Automation; using System.Text; namespace WindowsEventLab { public enum ResultType { PSObjectCollection = 0, DataTable = 1 } public class PowerShellHelper { public PowerShellHelper() { } //This works but only returns standard output as text and not an object but will still work to invoke full-fledged scripts //Object invokedResults = PowerShellHelper.InvokePowerShellScript(@"C:\MyDir\TestPoShScript.ps1"); public static Object InvokePowerShellScript(string scriptPath) { ProcessStartInfo startInfo = new ProcessStartInfo(); Process process = new Process(); Object returnValue = null; startInfo.FileName = @"powershell.exe"; startInfo.Arguments = (@"& 'PATH'").Replace("PATH", scriptPath); startInfo.RedirectStandardOutput = true; startInfo.RedirectStandardError = true; startInfo.UseShellExecute = false; startInfo.CreateNoWindow = true; process = new Process { StartInfo = startInfo, EnableRaisingEvents = true }; process.StartInfo.StandardOutputEncoding = Encoding.UTF8; process.StartInfo.StandardErrorEncoding = Encoding.UTF8; process.OutputDataReceived += new DataReceivedEventHandler ( delegate (object sender, DataReceivedEventArgs e) { //For some e.Data always has an empty string returnValue = e.Data; //using (StreamReader output = process.StandardOutput) //{ // standardOutput = output.ReadToEnd(); //} } ); process.Start(); //process.BeginOutputReadLine(); //This is starts reading the return value by invoking OutputDataReceived event handler process.WaitForExit(); Object standardOutput = process.StandardOutput.ReadToEnd(); //Assert.IsTrue(output.Contains("StringToBeVerifiedInAUnitTest")); String errors = process.StandardError.ReadToEnd(); //Assert.IsTrue(string.IsNullOrEmpty(errors)); process.Close(); //For some reason returnValue does not have the object type output //return returnValue; return standardOutput; } //IDictionary parameters = new Dictionary<String, String>(); //parameters.AddUser("Identity", "My-AD-Group-Name"); //Collection<object> results = PowerShellHelper.Execute(textBoxCommand.Text); //DataTable dataTable = PowerShellHelper.ToDataTable(results); public static Collection<object> ExecuteString(string command) //, IDictionary parameters { Collection<object> results = null; string error = ""; var ss = InitialSessionState.CreateDefault(); //ss.ImportPSModulesFromPath("C:\\WINDOWS\\system32\\WindowsPowerShell\\v1.0\\Modules\\ActiveDirectory\\ActiveDirectory.psd1"); //ss.ImportPSModulesFromPath(@"C:\WINDOWS\system32\WindowsPowerShell\v1.0\Modules\dbatools\dbatools.psm1"); //ss.ImportPSModule(new[] { "ActiveDirectory", "dbatools" }); using (var ps = PowerShell.Create(ss)) { //http://www.agilepointnxblog.com/powershell-error-there-is-no-runspace-available-to-run-scripts-in-this-thread/ // Exception getting "Path": "There is no Runspace available to run scripts in this thread. You can provide one in the DefaultRunspace property of the System.Management.Automation.Runspaces.Runspace type. The script block you attempted to invoke was: $this.Mainmodule.FileName" // //ps.AddCommand("[System.Net.ServicePointManager]::ServerCertificateValidationCallback = $null").Invoke(); //var rslt = ps.AddCommand("Import-Module").AddParameter("Name", "ActiveDirectory").Invoke(); //rslt = ps.AddCommand("Import-Module").AddParameter("Name", "dbatools").Invoke(); //rslt = ps.AddCommand("Import-Module").AddParameter("Name", "sqlps").Invoke(); if (ps.HadErrors) { System.Collections.ArrayList errorList = (System.Collections.ArrayList)ps.Runspace.SessionStateProxy.PSVariable.GetValue("Error"); error = string.Join("\n", errorList.ToArray()); throw new Exception(error); } ps.Commands.Clear(); PSInvocationSettings settings = new PSInvocationSettings(); settings.ErrorActionPreference = ActionPreference.Stop; //results = ps.AddCommand(command).AddParameters(parameters).Invoke<PSObject>(); //results = ps.AddCommand(command).AddParameters(parameters).Invoke<object>(); results = ps.AddScript(command).Invoke<object>(); if (ps.HadErrors == true) { System.Collections.ArrayList errorList = (System.Collections.ArrayList)ps.Runspace.SessionStateProxy.PSVariable.GetValue("Error"); error = string.Join("\n", errorList.ToArray()); throw new Exception(error); } foreach (var result in results) { Debug.WriteLine(result.ToString()); } } return results; } } }

這時候你會遇到 using System.Management.Automation; 系統竟然不認識,這時候我網路上找到保哥這篇文章

https://blog.miniasp.com/post/2014/05/09/Call-Windows-PowerShell-Cmdlets-from-CSharp-program

去修改專案檔,增加     <Reference Include="System.Management.Automation" />

就可以了,這點覺得很奇怪,不過原因我也沒有細查了


3. 修改 黑大的程式碼,主要我改變時間,我改撈取 12 小時候並且,最後我轉成 JSON ,改動幅度不大


Param ( # 查詢區間之起始時間(預設最近12小時內) [DateTime]$start = (Get-Date).AddHours(-12) ) $wp = [Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent() if (-Not $wp.IsInRole([Security.Principal.WindowsBuiltInRole]&quot;Administrator&quot;)) { Write-Host &quot;*** 請使用系統管理員權限執行 ***&quot; -ForegroundColor Red return } # 登入類別對照表 $LogonTypeTexts = @( &#39;NA&#39;,&#39;NA&#39;, &#39;Interactive&#39;, #2 &#39;Network&#39;,&#39;Batch&#39;,&#39;Service&#39;,&#39;NA&#39;,&#39;Unlock&#39;,&#39;NetworkClearText&#39;, &#39;NewCredentials&#39;,&#39;RemoteInteractive&#39;,&#39;CachedInteractive&#39; ) # 計算起始時間距今的毫秒數 $timeDiff = (New-TimeSpan -Start $start -End (Get-Date)).TotalMilliseconds # 限定 4624(登入成功)&#12289;4625(登入失敗) $xpath = @&quot; *[ System[ (EventID=4624 or EventID=4625) and TimeCreated[timediff(`@SystemTime) &lt;= $timeDiff] ] and EventData[ Data[@Name=&#39;IpAddress&#39;] != &#39;-&#39; and Data[@Name=&#39;IpAddress&#39;] != &#39;::1&#39; and Data[@Name=&#39;IpAddress&#39;] != &#39;127.0.0.1&#39; ] ] &quot;@ # 加上 SilentlyContinue 防止查無資料時噴錯 No events were found that match the specified selection criteria. Get-WinEvent -LogName &#39;Security&#39; -FilterXPath $xpath -ErrorAction SilentlyContinue | ForEach-Object { $xml = [xml]$_.ToXml() # 將事件記錄轉成 XML $d = @{} # 建立 Hashtable 放 EventData.Data 中的客製屬性 @($xml.Event.EventData.Data) | ForEach-Object { $d[$_.name] = $_.&#39;#text&#39; } if ($_.ID -eq 4624) { $action = &#39;登入成功&#39; } elseif ($_.ID -eq 4625) { $action = &#39;登入失敗&#39; } $logonType = &#39;&#39; if ($d.LogonType -gt 1) { $logonType = $LogonTypeTexts[$d.LogonType] } [PSCustomObject]@{ Action = $action; Time = $_.TimeCreated.ToString(&quot;yyyy/MM/dd HH:mm:ss&quot;); Id = $_.ID; TargetAccount = &quot;$($d.TargetDomainName)\$($d.TargetUserName)&quot;; # 登入帳號 Socket = &quot;$($d[&#39;IpAddress&#39;]):$($d[&#39;IpPort&#39;])&quot;; # IP 來源 LogonType = $logonType +&#39;:&#39;+ $d.LogonType ; # 登入類別 LogonProcess = $d.LogonProcessName; # 程序名稱 AuthPkgName = $d.AuthenticationPackageName; # 驗證模組 SubjectLogonId=$d.SubjectLogonId } } | ConvertTo-Json #Soruce From 黑暗執行緒: https://blog.darkthread.net/blog/ps-list-logon-events/ #我只是做了部分修改

4. 接下來你執行得時候,需要有高的管理權限,所以你必須要在專案中加入程式的執行授權,在 app.manifest

中加入    <requestedExecutionLevel level="requireAdministrator" uiAccess="false" /> 這時候才可以


5.主程式碼的部分

static void Main(string[] args) { //關閉驗證的Policy PowerShellHelper.ExecuteString("Set-ExecutionPolicy Unrestricted"); var res1 = PowerShellHelper.InvokePowerShellScript(AppDomain.CurrentDomain.BaseDirectory + "script1.ps1"); Console.WriteLine(res1); File.WriteAllText(AppDomain.CurrentDomain.BaseDirectory + "log.txt", res1.ToString()); Console.ReadLine(); }



我就放在 GITHUB 上面有需要的就自取吧,我主要是用 .Net Framework 4.8.1 寫的,沒有跨平台需求我就沒有用 .Net 6 了

Github : https://github.com/donma/WindowsEventLab

參考網站:

https://blog.miniasp.com/post/2014/05/09/Call-Windows-PowerShell-Cmdlets-from-CSharp-program

https://gist.github.com/SQLJana/1738dff1b151e2412240067167076e7a

https://cynthiachuang.github.io/Solve-that-Mandarin-Characters-Will-Be-Garbled-When-Using-Git-Command-in-Powershell/

https://hackercat.org/windows/powershell-cannot-be-loaded-because-the-execution-of-scripts-is-disabled-on-this-system

https://blog.darkthread.net/blog/ps-list-logon-events/

https://blog.darkthread.net/blog/ps-pipeline-exe-encoding/






當麻許的碎念筆記 2014 | Donma Hsu Design.