CLI tool for running Playbooks
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

639 lines
28 KiB

1 year ago
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Diagnostics;
  4. using System.IO;
  5. using System.Linq;
  6. using System.Management;
  7. using System.Security.AccessControl;
  8. using System.ServiceProcess;
  9. using System.Text.RegularExpressions;
  10. using System.Threading.Tasks;
  11. using TrustedUninstaller.Shared.Exceptions;
  12. using TrustedUninstaller.Shared.Tasks;
  13. using YamlDotNet.Serialization;
  14. namespace TrustedUninstaller.Shared.Actions
  15. {
  16. public class FileAction : ITaskAction
  17. {
  18. [YamlMember(typeof(string), Alias = "path")]
  19. public string RawPath { get; set; }
  20. [YamlMember(typeof(string), Alias = "prioritizeExe")]
  21. public bool ExeFirst { get; set; } = false;
  22. [YamlMember(typeof(string), Alias = "weight")]
  23. public int ProgressWeight { get; set; } = 2;
  24. [YamlMember(typeof(string), Alias = "useNSudoTI")]
  25. public bool TrustedInstaller { get; set; } = false;
  26. public int GetProgressWeight() => ProgressWeight;
  27. private bool InProgress { get; set; }
  28. public void ResetProgress() => InProgress = false;
  29. public string ErrorString() => $"FileAction failed to remove file or directory '{Environment.ExpandEnvironmentVariables(RawPath)}'.";
  30. private string GetRealPath()
  31. {
  32. return Environment.ExpandEnvironmentVariables(RawPath);
  33. }
  34. private string GetRealPath(string path)
  35. {
  36. return Environment.ExpandEnvironmentVariables(path);
  37. }
  38. public UninstallTaskStatus GetStatus()
  39. {
  40. if (InProgress) return UninstallTaskStatus.InProgress; var realPath = GetRealPath();
  41. if (realPath.Contains("*"))
  42. {
  43. var lastToken = realPath.LastIndexOf("\\");
  44. var parentPath = realPath.Remove(lastToken).TrimEnd('\\');
  45. // This is to prevent it from re-iterating with an incorrect argument
  46. if (parentPath.Contains("*")) return UninstallTaskStatus.Completed;
  47. var filter = realPath.Substring(lastToken + 1);
  48. if (Directory.Exists(parentPath) && (Directory.GetFiles(parentPath, filter).Any() || Directory.GetDirectories(parentPath, filter).Any()))
  49. {
  50. return UninstallTaskStatus.ToDo;
  51. }
  52. else return UninstallTaskStatus.Completed;
  53. }
  54. var isFile = File.Exists(realPath);
  55. var isDirectory = Directory.Exists(realPath);
  56. return isFile || isDirectory ? UninstallTaskStatus.ToDo : UninstallTaskStatus.Completed;
  57. }
  58. private async Task DeleteFile(string file, bool log = false)
  59. {
  60. if (!TrustedInstaller)
  61. {
  62. try {await Task.Run(() => File.Delete(file));} catch {}
  63. if (File.Exists(file))
  64. {
  65. CmdAction delAction = new CmdAction()
  66. {
  67. Command = $"del /q /f {file}"
  68. };
  69. await delAction.RunTask();
  70. }
  71. }
  72. else if (File.Exists("NSudoLC.exe"))
  73. {
  74. RunAction tiDelAction = new RunAction()
  75. {
  76. Exe = "NSudoLC.exe",
  77. Arguments = $"-U:T -P:E -M:S -Priority:RealTime -UseCurrentConsole -Wait cmd /c \"del /q /f \"{file}\"\"",
  78. BaseDir = true,
  79. CreateWindow = false
  80. };
  81. await tiDelAction.RunTask();
  82. if (tiDelAction.Output != null)
  83. {
  84. if (log) ErrorLogger.WriteToErrorLog(tiDelAction.Output, Environment.StackTrace,
  85. $"FileAction Error", file);
  86. }
  87. }
  88. else
  89. {
  90. ErrorLogger.WriteToErrorLog($"NSudo was invoked with no supplied NSudo executable.", Environment.StackTrace,
  91. $"FileAction Error", file);
  92. }
  93. }
  94. private async Task RemoveDirectory(string dir, bool log = false)
  95. {
  96. if (!TrustedInstaller)
  97. {
  98. try { Directory.Delete(dir, true); } catch { }
  99. if (Directory.Exists(dir))
  100. {
  101. Console.WriteLine("Directory still exists.. trying second method.");
  102. var deleteDirCmd = new CmdAction()
  103. {
  104. Command = $"rmdir /Q /S \"{dir}\""
  105. };
  106. await deleteDirCmd.RunTask();
  107. if (deleteDirCmd.StandardError != null)
  108. {
  109. Console.WriteLine($"Error Output: {deleteDirCmd.StandardError}");
  110. }
  111. if (deleteDirCmd.StandardOutput != null)
  112. {
  113. Console.WriteLine($"Standard Output: {deleteDirCmd.StandardOutput}");
  114. }
  115. }
  116. }
  117. else if (File.Exists("NSudoLC.exe"))
  118. {
  119. RunAction tiDelAction = new RunAction()
  120. {
  121. Exe = "NSudoLC.exe",
  122. Arguments = $"-U:T -P:E -M:S -Priority:RealTime -UseCurrentConsole -Wait cmd /c \"rmdir /q /s \"{dir}\"\"",
  123. BaseDir = true,
  124. CreateWindow = false
  125. };
  126. await tiDelAction.RunTask();
  127. if (tiDelAction.Output != null)
  128. {
  129. if (log) ErrorLogger.WriteToErrorLog(tiDelAction.Output, Environment.StackTrace,
  130. $"FileAction Error", dir);
  131. }
  132. }
  133. else
  134. {
  135. ErrorLogger.WriteToErrorLog($"NSudo was invoked with no supplied NSudo executable.", Environment.StackTrace,
  136. $"FileAction Error", dir);
  137. }
  138. }
  139. private async Task DeleteItemsInDirectory(string dir, string filter = "*")
  140. {
  141. var realPath = GetRealPath(dir);
  142. var files = Directory.EnumerateFiles(realPath, filter);
  143. var directories = Directory.EnumerateDirectories(realPath, filter);
  144. if (ExeFirst) files = files.ToList().OrderByDescending(x => x.EndsWith(".exe"));
  145. var lockedFilesList = new List<string> { "MpOAV.dll", "MsMpLics.dll", "EppManifest.dll", "MpAsDesc.dll", "MpClient.dll", "MsMpEng.exe" };
  146. foreach (var file in files)
  147. {
  148. Console.WriteLine($"Deleting {file}...");
  149. System.GC.Collect();
  150. System.GC.WaitForPendingFinalizers();
  151. await DeleteFile(file);
  152. if (File.Exists(file))
  153. {
  154. TaskKillAction taskKillAction = new TaskKillAction();
  155. if (file.EndsWith(".sys"))
  156. {
  157. var driverService = Path.GetFileNameWithoutExtension(file);
  158. try
  159. {
  160. //ServiceAction won't work here due to it not being able to detect driver services.
  161. var cmdAction = new CmdAction();
  162. Console.WriteLine($"Removing driver service {driverService}...");
  163. cmdAction.Command = Environment.Is64BitOperatingSystem ?
  164. $"ProcessHacker\\x64\\ProcessHacker.exe -s -elevate -c -ctype service -cobject {driverService} -caction stop" :
  165. $"ProcessHacker\\x86\\ProcessHacker.exe -s -elevate -c -ctype service -cobject {driverService} -caction stop";
  166. await cmdAction.RunTask();
  167. cmdAction.Command = Environment.Is64BitOperatingSystem ?
  168. $"ProcessHacker\\x64\\ProcessHacker.exe -s -elevate -c -ctype service -cobject {driverService} -caction delete" :
  169. $"ProcessHacker\\x86\\ProcessHacker.exe -s -elevate -c -ctype service -cobject {driverService} -caction delete";
  170. await cmdAction.RunTask();
  171. }
  172. catch (Exception servException)
  173. {
  174. ErrorLogger.WriteToErrorLog(servException.Message, servException.StackTrace,
  175. $"FileAction Error: Error while trying to delete driver service {driverService}.", file);
  176. }
  177. }
  178. if (lockedFilesList.Contains(Path.GetFileName(file)))
  179. {
  180. TaskKillAction killAction = new TaskKillAction()
  181. {
  182. ProcessName = "MsMpEng"
  183. };
  184. await killAction.RunTask();
  185. killAction.ProcessName = "NisSrv";
  186. await killAction.RunTask();
  187. killAction.ProcessName = "SecurityHealthService";
  188. await killAction.RunTask();
  189. killAction.ProcessName = "smartscreen";
  190. await killAction.RunTask();
  191. }
  192. var processes = new List<Process>();
  193. try
  194. {
  195. processes = WinUtil.WhoIsLocking(file);
  196. }
  197. catch (Exception e)
  198. {
  199. ErrorLogger.WriteToErrorLog(e.Message, e.StackTrace,
  200. $"FileAction Error", file);
  201. }
  202. var delay = 0;
  203. int svcCount = 0;
  204. foreach (var svchost in processes.Where(x => x.ProcessName.Equals("svchost")))
  205. {
  206. try
  207. {
  208. using var search = new ManagementObjectSearcher($"select * from Win32_Service where ProcessId = '{svchost.Id}'");
  209. foreach (ManagementObject queryObj in search.Get())
  210. {
  211. var serviceName = (string)queryObj["Name"]; // Access service name
  212. var serv = ServiceController.GetServices().FirstOrDefault(x => x.ServiceName.Equals(serviceName));
  213. if (serv == null) svcCount++;
  214. else svcCount += serv.DependentServices.Length + 1;
  215. }
  216. } catch (Exception e)
  217. {
  218. Console.WriteLine($"\r\nError: Could not get amount of services locking file.\r\nException: " + e.Message);
  219. }
  220. }
  221. if (svcCount > 8) Console.WriteLine("Amount of locking services exceeds 8, skipping...");
  222. while (processes.Any() && delay <= 800 && svcCount <= 8)
  223. {
  224. Console.WriteLine("Processes locking the file:");
  225. foreach (var process in processes)
  226. {
  227. Console.WriteLine(process.ProcessName);
  228. }
  229. foreach (var process in processes)
  230. {
  231. try
  232. {
  233. if (process.ProcessName.Equals("TrustedUninstaller.CLI"))
  234. {
  235. Console.WriteLine("Skipping TU.CLI...");
  236. continue;
  237. }
  238. if (Regex.Match(process.ProcessName, "ame.?wizard", RegexOptions.IgnoreCase).Success)
  239. {
  240. Console.WriteLine("Skipping AME Wizard...");
  241. continue;
  242. }
  243. taskKillAction.ProcessName = process.ProcessName;
  244. taskKillAction.ProcessID = process.Id;
  245. Console.WriteLine($"Killing locking process {process.ProcessName} with PID {process.Id}...");
  246. }
  247. catch (InvalidOperationException)
  248. {
  249. // Calling ProcessName on a process object that has exited will thrown this exception causing the
  250. // entire loop to abort. Since killing a process takes a bit of time, another process in the loop
  251. // could exit during that time. This accounts for that.
  252. continue;
  253. }
  254. await taskKillAction.RunTask();
  255. }
  256. // This gives any obstinant processes some time to unlock the file on their own.
  257. //
  258. // This could be done above but it's likely to cause HasExited errors if delays are
  259. // introduced after WhoIsLocking.
  260. System.Threading.Thread.Sleep(delay);
  261. try
  262. {
  263. processes = WinUtil.WhoIsLocking(file);
  264. }
  265. catch (Exception e)
  266. {
  267. ErrorLogger.WriteToErrorLog(e.Message, e.StackTrace,
  268. $"FileAction Error", file);
  269. }
  270. delay += 100;
  271. }
  272. if (delay >= 800)
  273. ErrorLogger.WriteToErrorLog($"Could not kill locking processes for file '{file}'. Process termination loop exceeded max cycles (8).",
  274. Environment.StackTrace, "FileAction Error");
  275. await DeleteFile(file, true);
  276. using (var writer = new StreamWriter("Logs\\FileChecklist.txt", true))
  277. {
  278. writer.WriteLine($"File Path: {file}\r\nDeleted: {!File.Exists(file)}\r\n" +
  279. $"======================");
  280. }
  281. }
  282. }
  283. //Loop through any subdirectories
  284. foreach (var directory in directories)
  285. {
  286. //Deletes the content of the directory
  287. await DeleteItemsInDirectory(directory);
  288. System.GC.Collect();
  289. System.GC.WaitForPendingFinalizers();
  290. await RemoveDirectory(directory, true);
  291. if (Directory.Exists(directory))
  292. ErrorLogger.WriteToErrorLog($"Could not remove directory '{directory}'.",
  293. Environment.StackTrace, $"FileAction Error");
  294. }
  295. }
  296. public async Task<bool> RunTask()
  297. {
  298. if (InProgress) throw new TaskInProgressException("Another File action was called while one was in progress.");
  299. InProgress = true;
  300. var realPath = GetRealPath();
  301. Console.WriteLine($"Removing file or directory '{realPath}'...");
  302. if (realPath.Contains("*"))
  303. {
  304. var lastToken = realPath.LastIndexOf("\\");
  305. var parentPath = realPath.Remove(lastToken).TrimEnd('\\');
  306. if (parentPath.Contains("*")) throw new ArgumentException("Parent directories to a given file filter cannot contain wildcards.");
  307. var filter = realPath.Substring(lastToken + 1);
  308. await DeleteItemsInDirectory(parentPath, filter);
  309. InProgress = false;
  310. return true;
  311. }
  312. var isFile = File.Exists(realPath);
  313. var isDirectory = Directory.Exists(realPath);
  314. if (isDirectory)
  315. {
  316. System.GC.Collect();
  317. System.GC.WaitForPendingFinalizers();
  318. await RemoveDirectory(realPath);
  319. if (Directory.Exists(realPath))
  320. {
  321. CmdAction permAction = new CmdAction()
  322. {
  323. Command = $"takeown /f \"{realPath}\" /r /d Y>NUL & icacls \"{realPath}\" /t /grant Administrators:F /c > NUL",
  324. Timeout = 5000
  325. };
  326. try
  327. {
  328. await permAction.RunTask();
  329. }
  330. catch (Exception e)
  331. {
  332. ErrorLogger.WriteToErrorLog(e.Message, e.StackTrace, "FileAction Error", realPath);
  333. }
  334. try
  335. {
  336. if (realPath.Contains("Defender"))
  337. {
  338. TaskKillAction killAction = new TaskKillAction()
  339. {
  340. ProcessName = "MsMpEng"
  341. };
  342. await killAction.RunTask();
  343. killAction.ProcessName = "NisSrv";
  344. await killAction.RunTask();
  345. killAction.ProcessName = "SecurityHealthService";
  346. await killAction.RunTask();
  347. killAction.ProcessName = "smartscreen";
  348. await killAction.RunTask();
  349. }
  350. }
  351. catch (Exception e)
  352. {
  353. ErrorLogger.WriteToErrorLog(e.Message, e.StackTrace,
  354. $"FileAction Error", realPath);
  355. }
  356. await RemoveDirectory(realPath, true);
  357. if (Directory.Exists(realPath))
  358. {
  359. //Delete the files in the initial directory. DOES delete directories.
  360. await DeleteItemsInDirectory(realPath);
  361. System.GC.Collect();
  362. System.GC.WaitForPendingFinalizers();
  363. await RemoveDirectory(realPath, true);
  364. }
  365. }
  366. }
  367. else if (isFile)
  368. {
  369. try
  370. {
  371. var lockedFilesList = new List<string> { "MpOAV.dll", "MsMpLics.dll", "EppManifest.dll", "MpAsDesc.dll", "MpClient.dll", "MsMpEng.exe" };
  372. var fileName = realPath.Split('\\').LastOrDefault();
  373. System.GC.Collect();
  374. System.GC.WaitForPendingFinalizers();
  375. await DeleteFile(realPath);
  376. if (File.Exists(realPath))
  377. {
  378. CmdAction permAction = new CmdAction()
  379. {
  380. Command = $"takeown /f \"{realPath}\" /r /d Y>NUL & icacls \"{realPath}\" /t /grant Administrators:F /c > NUL",
  381. Timeout = 5000
  382. };
  383. try
  384. {
  385. await permAction.RunTask();
  386. }
  387. catch (Exception e)
  388. {
  389. ErrorLogger.WriteToErrorLog(e.Message, e.StackTrace, "FileAction Error", realPath);
  390. }
  391. TaskKillAction taskKillAction = new TaskKillAction();
  392. if (realPath.EndsWith(".sys"))
  393. {
  394. var driverService = Path.GetFileNameWithoutExtension(realPath);
  395. try
  396. {
  397. //ServiceAction won't work here due to it not being able to detect driver services.
  398. var cmdAction = new CmdAction();
  399. Console.WriteLine($"Removing driver service {driverService}...");
  400. cmdAction.Command = Environment.Is64BitOperatingSystem ?
  401. $"ProcessHacker\\x64\\ProcessHacker.exe -s -elevate -c -ctype service -cobject {driverService} -caction stop" :
  402. $"ProcessHacker\\x86\\ProcessHacker.exe -s -elevate -c -ctype service -cobject {driverService} -caction stop";
  403. await cmdAction.RunTask();
  404. cmdAction.Command = Environment.Is64BitOperatingSystem ?
  405. $"ProcessHacker\\x64\\ProcessHacker.exe -s -elevate -c -ctype service -cobject {driverService} -caction delete" :
  406. $"ProcessHacker\\x86\\ProcessHacker.exe -s -elevate -c -ctype service -cobject {driverService} -caction delete";
  407. await cmdAction.RunTask();
  408. }
  409. catch (Exception servException)
  410. {
  411. ErrorLogger.WriteToErrorLog(servException.Message, servException.StackTrace,
  412. $"FileAction Error: Error trying to delete driver service {driverService}.", realPath);
  413. }
  414. }
  415. if (lockedFilesList.Contains(fileName))
  416. {
  417. TaskKillAction killAction = new TaskKillAction()
  418. {
  419. ProcessName = "MsMpEng"
  420. };
  421. await killAction.RunTask();
  422. killAction.ProcessName = "NisSrv";
  423. await killAction.RunTask();
  424. killAction.ProcessName = "SecurityHealthService";
  425. await killAction.RunTask();
  426. killAction.ProcessName = "smartscreen";
  427. await killAction.RunTask();
  428. }
  429. var processes = new List<Process>();
  430. try
  431. {
  432. processes = WinUtil.WhoIsLocking(realPath);
  433. }
  434. catch (Exception e)
  435. {
  436. ErrorLogger.WriteToErrorLog(e.Message, e.StackTrace,
  437. $"FileAction Error", realPath);
  438. }
  439. var delay = 0;
  440. int svcCount = 0;
  441. foreach (var svchost in processes.Where(x => x.ProcessName.Equals("svchost")))
  442. {
  443. try
  444. {
  445. using var search = new ManagementObjectSearcher($"select * from Win32_Service where ProcessId = '{svchost.Id}'");
  446. foreach (ManagementObject queryObj in search.Get())
  447. {
  448. var serviceName = (string)queryObj["Name"]; // Access service name
  449. var serv = ServiceController.GetServices().FirstOrDefault(x => x.ServiceName.Equals(serviceName));
  450. if (serv == null) svcCount++;
  451. else svcCount += serv.DependentServices.Length + 1;
  452. }
  453. } catch (Exception e)
  454. {
  455. Console.WriteLine($"\r\nError: Could not get amount of services locking file.\r\nException: " + e.Message);
  456. }
  457. }
  458. if (svcCount > 8) Console.WriteLine("Amount of locking services exceeds 8, skipping...");
  459. while (processes.Any() && delay <= 800 && svcCount <= 8)
  460. {
  461. Console.WriteLine("Processes locking the file:");
  462. foreach (var process in processes)
  463. {
  464. Console.WriteLine(process.ProcessName);
  465. }
  466. foreach (var process in processes)
  467. {
  468. try
  469. {
  470. if (process.ProcessName.Equals("TrustedUninstaller.CLI"))
  471. {
  472. Console.WriteLine("Skipping TU.CLI...");
  473. continue;
  474. }
  475. if (Regex.Match(process.ProcessName, "ame.?wizard", RegexOptions.IgnoreCase).Success)
  476. {
  477. Console.WriteLine("Skipping AME Wizard...");
  478. continue;
  479. }
  480. taskKillAction.ProcessName = process.ProcessName;
  481. taskKillAction.ProcessID = process.Id;
  482. Console.WriteLine($"Killing {process.ProcessName} with PID {process.Id}... it is locking {realPath}");
  483. }
  484. catch (InvalidOperationException)
  485. {
  486. // Calling ProcessName on a process object that has exited will thrown this exception causing the
  487. // entire loop to abort. Since killing a process takes a bit of time, another process in the loop
  488. // could exit during that time. This accounts for that.
  489. continue;
  490. }
  491. try
  492. {
  493. await taskKillAction.RunTask();
  494. }
  495. catch (Exception e)
  496. {
  497. ErrorLogger.WriteToErrorLog(e.Message, e.StackTrace,
  498. $"FileAction Error: Could not kill process {process.ProcessName}.");
  499. }
  500. }
  501. // This gives any obstinant processes some time to unlock the file on their own.
  502. //
  503. // This could be done above but it's likely to cause HasExited errors if delays are
  504. // introduced after WhoIsLocking.
  505. System.Threading.Thread.Sleep(delay);
  506. try
  507. {
  508. processes = WinUtil.WhoIsLocking(realPath);
  509. }
  510. catch (Exception e)
  511. {
  512. ErrorLogger.WriteToErrorLog(e.Message, e.StackTrace,
  513. $"FileAction Error", realPath);
  514. }
  515. delay += 100;
  516. }
  517. if (delay >= 800)
  518. ErrorLogger.WriteToErrorLog($"Could not kill locking processes for file '{realPath}'. Process termination loop exceeded max cycles (8).",
  519. Environment.StackTrace, "FileAction Error");
  520. await DeleteFile(realPath, true);
  521. }
  522. }
  523. catch (Exception e)
  524. {
  525. ErrorLogger.WriteToErrorLog(e.Message, e.StackTrace,
  526. $"FileAction Error: Error while trying to delete {realPath}.");
  527. }
  528. using (var writer = new StreamWriter("Logs\\FileChecklist.txt", true))
  529. {
  530. writer.WriteLine($"File Path: {realPath}\r\nDeleted: {!File.Exists(realPath)}\r\n" +
  531. $"======================");
  532. }
  533. }
  534. else
  535. {
  536. Console.WriteLine($"File or directory '{realPath}' not found.");
  537. }
  538. InProgress = false;
  539. return true;
  540. }
  541. }
  542. }