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.

597 lines
26 KiB

1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
  1. using Newtonsoft.Json.Linq;
  2. using System;
  3. using System.Collections.Generic;
  4. using System.IO;
  5. using System.IO.MemoryMappedFiles;
  6. using System.Linq;
  7. using System.Net;
  8. using System.Net.Http;
  9. using System.Runtime.InteropServices;
  10. using System.ServiceProcess;
  11. using System.Text;
  12. using System.Threading;
  13. using System.Threading.Tasks;
  14. using System.Xml;
  15. using System.Xml.Serialization;
  16. using TrustedUninstaller.Shared.Actions;
  17. using TrustedUninstaller.Shared.Parser;
  18. using TrustedUninstaller.Shared.Tasks;
  19. namespace TrustedUninstaller.Shared
  20. {
  21. public static class AmeliorationUtil
  22. {
  23. private static readonly ConfigParser Parser = new ConfigParser();
  24. private static readonly HttpClient Client = new HttpClient();
  25. public static Playbook Playbook { set; get; }
  26. public static readonly List<string> ErrorDisplayList = new List<string>();
  27. public static int GetProgressMaximum()
  28. {
  29. return Parser.Tasks.Sum(task => task.Actions.Sum(action => action.GetProgressWeight()));
  30. }
  31. public static bool AddTasks(string configPath, string file)
  32. {
  33. try
  34. {
  35. //This allows for a proper detection of if any error occurred, and if so the CLI will relay an :AME-Fatal Error:
  36. //This is important, as we want the process to stop immediately if a YAML syntax error was detected.
  37. bool hadError = false;
  38. //Adds the config file to the parser's task list
  39. Parser.Add(Path.Combine(configPath, file));
  40. var currentTask = Parser.Tasks[Parser.Tasks.Count - 1];
  41. if (File.Exists("TasksAdded.txt"))
  42. {
  43. var doneTasks = File.ReadAllText("TasksAdded.txt").Split(new[] { "\r\n" }, StringSplitOptions.None);
  44. if (doneTasks.Contains(currentTask.Title))
  45. {
  46. Parser.Tasks.Remove(currentTask);
  47. return true;
  48. }
  49. }
  50. //Get the features of the last added task (the task that was just added from the config file)
  51. var features = currentTask.Features;
  52. //Each feature would reference a directory that has a YAML file, we take those directories and then run the
  53. //AddTasks function again, until we reach a file that doesn't reference any other YAML files, and add them
  54. //all to the parser's tasks list.
  55. if (features == null) return true;
  56. foreach (var feature in features)
  57. {
  58. var subResult = AddTasks(configPath, feature);
  59. // We could return false here, however we want to output ALL detected YAML errors,
  60. // which is why we continue here.
  61. if (!subResult) hadError = true;
  62. }
  63. return hadError ? false : true;
  64. }
  65. catch (Exception e)
  66. {
  67. Console.WriteLine($"Error adding tasks in {configPath + "\\" + file}:\r\n{e.Message}");
  68. ErrorLogger.WriteToErrorLog(e.Message, e.StackTrace, $"Error adding tasks in {configPath + "\\" + file}.");
  69. return false;
  70. }
  71. }
  72. public static async Task<int> DoActions(UninstallTask task, UninstallTaskPrivilege privilege)
  73. {
  74. try
  75. {
  76. //If the privilege is admin and the program is running as TI, do not do the action.
  77. if (privilege == UninstallTaskPrivilege.Admin && WinUtil.IsTrustedInstaller())
  78. {
  79. return 0;
  80. }
  81. if (privilege == UninstallTaskPrivilege.TrustedInstaller && !WinUtil.IsTrustedInstaller())
  82. {
  83. Console.WriteLine("Relaunching as Trusted Installer!");
  84. var mmf = MemoryMappedFile.CreateNew("ImgA", 5000000);
  85. WinUtil.RelaunchAsTrustedInstaller();
  86. if (NativeProcess.Process == null)
  87. {
  88. ErrorLogger.WriteToErrorLog($"Could not launch TrustedInstaller process. Return output was null.",
  89. Environment.StackTrace, "Error while attempting to sync with TrustedInstaller process.");
  90. Console.WriteLine(":AME-Fatal Error: Could not launch TrustedInstaller process.");
  91. Environment.Exit(-1);
  92. }
  93. var delay = 20;
  94. while (!NativeProcess.Process.HasExited)
  95. {
  96. if (delay > 3500)
  97. {
  98. NativeProcess.Process.Kill();
  99. ErrorLogger.WriteToErrorLog($"Could not initialize memory data exchange. Timeframe exceeded.",
  100. Environment.StackTrace, "Error while attempting to sync with TrustedInstaller process.");
  101. Console.WriteLine(":AME-Fatal Error: Could not initialize memory data exchange.");
  102. Environment.Exit(-1);
  103. }
  104. Task.Delay(delay).Wait();
  105. // Kind of inefficient looping this, however it's likely to cause access errors otherwise
  106. using var stream = mmf.CreateViewStream();
  107. using BinaryReader binReader = new BinaryReader(stream);
  108. {
  109. var res = binReader.ReadBytes((int)stream.Length);
  110. var data = Encoding.UTF8.GetString(res);
  111. var end = data.IndexOf('\0');
  112. if (end == 0)
  113. {
  114. delay += 200;
  115. }
  116. else
  117. {
  118. break;
  119. }
  120. }
  121. }
  122. var offset = 0;
  123. var read = false;
  124. using (var stream = mmf.CreateViewStream())
  125. {
  126. while (!NativeProcess.Process.HasExited || read)
  127. {
  128. read = false;
  129. BinaryReader binReader = new BinaryReader(stream);
  130. binReader.BaseStream.Seek(offset, SeekOrigin.Begin);
  131. var res = binReader.ReadBytes((int)stream.Length - offset);
  132. var data = Encoding.UTF8.GetString(res);
  133. var end = data.IndexOf("\0");
  134. var content = data.Substring(0, end);
  135. offset += Encoding.UTF8.GetBytes(content).Length;
  136. var output = content.Split(new [] {Environment.NewLine}, StringSplitOptions.None);
  137. if (output.Length > 0) output = output.Take(output.Length - 1).ToArray();
  138. foreach (var line in output)
  139. {
  140. Console.WriteLine(line);
  141. read = true;
  142. // Introducing ANY delay here makes it lag behind, which isn't ideal
  143. //Task.Delay(5).Wait();
  144. }
  145. Task.Delay(20).Wait();
  146. }
  147. }
  148. mmf.Dispose();
  149. return 0; //Only returns after TI is done
  150. }
  151. //Goes through the list of tasks that are inside the parser class,
  152. //and runs the task using the RunTask method
  153. //Check the Actions folder inside the Shared folder for reference.
  154. foreach (ITaskAction action in task.Actions)
  155. {
  156. int i = 0;
  157. //var actionType = action.GetType().ToString().Replace("TrustedUninstaller.Shared.Actions.", "");
  158. try
  159. {
  160. do
  161. {
  162. //Console.WriteLine($"Running {actionType}");
  163. Console.WriteLine();
  164. try
  165. {
  166. await action.RunTask();
  167. action.ResetProgress();
  168. }
  169. catch (Exception e)
  170. {
  171. action.ResetProgress();
  172. if (e.InnerException != null)
  173. {
  174. ErrorLogger.WriteToErrorLog(e.InnerException.Message, e.InnerException.StackTrace, e.Message);
  175. }
  176. else
  177. {
  178. ErrorLogger.WriteToErrorLog(e.Message, e.StackTrace, action.ErrorString());
  179. List<string> ExceptionBreakList = new List<string>() { "System.ArgumentException", "System.SecurityException", "System.UnauthorizedAccessException", "System.UnauthorizedAccessException", "System.TimeoutException" };
  180. if (ExceptionBreakList.Any(x => x.Equals(e.GetType().ToString())))
  181. {
  182. i = 10;
  183. break;
  184. }
  185. }
  186. }
  187. Console.WriteLine($"Status: {action.GetStatus()}");
  188. if (i > 0) Thread.Sleep(50);
  189. i++;
  190. } while (action.GetStatus() != UninstallTaskStatus.Completed && i < 10);
  191. }
  192. catch (Exception e)
  193. {
  194. ErrorLogger.WriteToErrorLog(e.Message, e.StackTrace, "Critical error while running action.");
  195. Console.WriteLine($":AME-ERROR: Critical error while running action: " + e.Message);
  196. }
  197. if (i == 10)
  198. {
  199. var errorString = action.ErrorString();
  200. ErrorLogger.WriteToErrorLog(errorString, Environment.StackTrace, "Action failed to complete.");
  201. // AmeliorationUtil.ErrorDisplayList.Add(errorString) would NOT work here since this
  202. // might be a separate process, and thus has to be forwarded via the console
  203. Console.WriteLine($":AME-ERROR: {errorString}");
  204. //Environment.Exit(-2);
  205. Console.WriteLine($"Action completed. Weight:{action.GetProgressWeight()}");
  206. continue;
  207. }
  208. Console.WriteLine($"Action completed. Weight:{action.GetProgressWeight()}");
  209. }
  210. Console.WriteLine("Task completed.");
  211. File.AppendAllText("TasksAdded.txt", task.Title + Environment.NewLine);
  212. }
  213. catch (Exception e)
  214. {
  215. ErrorLogger.WriteToErrorLog(e.Message, e.StackTrace,
  216. "Encountered an error while doing task actions.");
  217. }
  218. return 0;
  219. }
  220. public static Task<Playbook> DeserializePlaybook(string dir)
  221. {
  222. Playbook pb;
  223. XmlSerializer serializer = new XmlSerializer(typeof(Playbook));
  224. using (XmlReader reader = XmlReader.Create($"{dir}\\playbook.conf"))
  225. {
  226. pb = (Playbook)serializer.Deserialize(reader);
  227. }
  228. pb.Path = dir;
  229. return Task.FromResult(pb);
  230. }
  231. public static async Task<int> StartAmelioration()
  232. {
  233. //Needed after defender removal's reboot, the "current directory" will be set to System32
  234. //After the auto start up.
  235. Directory.SetCurrentDirectory(AppDomain.CurrentDomain.BaseDirectory);
  236. if (File.Exists("TasksAdded.txt") && !WinUtil.IsTrustedInstaller())
  237. {
  238. File.Delete("TasksAdded.txt");
  239. }
  240. if (Directory.Exists("Logs") && !WinUtil.IsTrustedInstaller())
  241. {
  242. if (File.Exists("Logs\\AdminOutput.txt"))
  243. {
  244. File.Delete("Logs\\AdminOutput.txt");
  245. }
  246. if (File.Exists("Logs\\TIOutput.txt"))
  247. {
  248. File.Delete("Logs\\TIOutput.txt");
  249. }
  250. if (File.Exists("Logs\\FileChecklist.txt"))
  251. {
  252. File.Delete("Logs\\FileChecklist.txt");
  253. }
  254. }
  255. //Check if KPH is installed.
  256. ServiceController service = ServiceController.GetDevices()
  257. .FirstOrDefault(s => s.DisplayName == "KProcessHacker2");
  258. if (service == null)
  259. {
  260. //Installs KPH
  261. await WinUtil.RemoveProtectionAsync();
  262. }
  263. var langsFile = Path.Combine($"{Playbook.Path}\\Configuration", "langs.txt");
  264. //Download language packs that were selected by the user
  265. if (!File.Exists(langsFile))
  266. {
  267. File.Create(langsFile);
  268. }
  269. //var langsSelected = File.ReadLines(langsFile);
  270. //await DownloadLanguagesAsync(langsSelected);
  271. //Start adding tasks from the top level configuration folder.
  272. if (!AddTasks($"{Playbook.Path}\\Configuration", "custom.yml"))
  273. {
  274. Console.WriteLine($":AME-Fatal Error: Error adding tasks.");
  275. Environment.Exit(1);
  276. }
  277. if (!Parser.Tasks.Any())
  278. {
  279. Console.Error.WriteLine($"Couldn't find any tasks.");
  280. return -1;
  281. }
  282. //Sort the list based on the priority value.
  283. if (Parser.Tasks.Any(x => x.Priority != Parser.Tasks.First().Priority))
  284. Parser.Tasks.Sort(new TaskComparer());
  285. UninstallTaskPrivilege prevPriv = UninstallTaskPrivilege.Admin;
  286. foreach (var task in Parser.Tasks.Where(task => task.Actions.Count != 0))
  287. {
  288. try
  289. {
  290. if (prevPriv == UninstallTaskPrivilege.TrustedInstaller && task.Privilege == UninstallTaskPrivilege.TrustedInstaller && !WinUtil.IsTrustedInstaller())
  291. {
  292. continue;
  293. }
  294. await DoActions(task, task.Privilege);
  295. prevPriv = task.Privilege;
  296. }
  297. catch (Exception ex)
  298. {
  299. ErrorLogger.WriteToErrorLog(ex.Message, ex.StackTrace, "Error during DoAction loop.");
  300. }
  301. }
  302. if (WinUtil.IsTrustedInstaller()) return 0;
  303. WinUtil.RegistryManager.UnhookUserHives();
  304. //Check how many files were successfully and unsuccessfully deleted.
  305. var deletedItemsCount = 0;
  306. var failedDeletedItemsCount = 0;
  307. if (File.Exists("Logs\\FileChecklist.txt"))
  308. {
  309. using (var reader = new StreamReader("Logs\\FileChecklist.txt"))
  310. {
  311. var data = reader.ReadToEnd();
  312. var listData = data.Split(new [] { Environment.NewLine }, StringSplitOptions.None).ToList();
  313. deletedItemsCount = listData.FindAll(s => s == "Deleted: True").Count();
  314. failedDeletedItemsCount = listData.FindAll(s => s == "Deleted: False").Count();
  315. }
  316. using (var writer = new StreamWriter("Logs\\FileChecklist.txt", true))
  317. {
  318. writer.WriteLine($"{deletedItemsCount} files were deleted successfully. " +
  319. $"{failedDeletedItemsCount} files couldn't be deleted.");
  320. }
  321. }
  322. Console.WriteLine($"{deletedItemsCount} files were deleted successfully. " +
  323. $"{failedDeletedItemsCount} files couldn't be deleted.");
  324. //Check if the kernel driver is installed.
  325. //service = ServiceController.GetDevices()
  326. //.FirstOrDefault(s => s.DisplayName == "KProcessHacker2");
  327. if (true)
  328. {
  329. //Remove Process Hacker's kernel driver.
  330. await WinUtil.UninstallDriver();
  331. }
  332. await AmeliorationUtil.SafeRunAction(new RegistryKeyAction()
  333. {
  334. KeyName = @"HKLM\SYSTEM\CurrentControlSet\Services\KProcessHacker2",
  335. });
  336. File.Delete("TasksAdded.txt");
  337. Console.WriteLine();
  338. Console.WriteLine("Playbook finished.");
  339. return 0;
  340. }
  341. public static async Task DownloadLanguagesAsync(IEnumerable<string> langsSelected)
  342. {
  343. foreach (var lang in langsSelected)
  344. {
  345. var lowerLang = lang.ToLower();
  346. var arch = RuntimeInformation.OSArchitecture;
  347. var winVersion = Environment.OSVersion.Version.Build;
  348. var convertedArch = "";
  349. switch (arch)
  350. {
  351. case Architecture.X64:
  352. convertedArch = "amd64";
  353. break;
  354. case Architecture.Arm64:
  355. convertedArch = "arm64";
  356. break;
  357. case Architecture.X86:
  358. convertedArch = "x86";
  359. break;
  360. }
  361. var uuidOfWindowsVersion = "";
  362. var uuidResponse =
  363. await Client.GetAsync(
  364. $"https://api.uupdump.net/listid.php?search={winVersion}%20{convertedArch}&sortByDate=1");
  365. switch (uuidResponse.StatusCode)
  366. {
  367. //200 Status code
  368. case HttpStatusCode.OK:
  369. {
  370. var result = uuidResponse.Content.ReadAsStringAsync().Result;
  371. //Gets the UUID of the first build object in the response, we take the first since it's the newest.
  372. uuidOfWindowsVersion = (string)(JToken.Parse(result)["response"]?["builds"]?.Children().First()
  373. .Children().First().Last());
  374. break;
  375. }
  376. //400 Status code
  377. case HttpStatusCode.BadRequest:
  378. {
  379. var result = uuidResponse.Content.ReadAsStringAsync().Result;
  380. dynamic data = JObject.Parse(result);
  381. Console.WriteLine($"Bad request.\r\nError:{data["response"]["error"]}");
  382. break;
  383. }
  384. //429 Status code
  385. case (HttpStatusCode)429:
  386. {
  387. var result = uuidResponse.Content.ReadAsStringAsync().Result;
  388. dynamic data = JObject.Parse(result);
  389. Console.WriteLine($"Too many requests, try again later.\r\nError:{data["response"]["error"]}");
  390. break;
  391. }
  392. //500 Status code
  393. case HttpStatusCode.InternalServerError:
  394. {
  395. var result = uuidResponse.Content.ReadAsStringAsync().Result;
  396. dynamic data = JObject.Parse(result);
  397. Console.WriteLine($"Internal Server Error.\r\nError:{data["response"]["error"]}");
  398. break;
  399. }
  400. default:
  401. throw new ArgumentOutOfRangeException();
  402. }
  403. var responseString =
  404. await Client.GetAsync(
  405. $"https://api.uupdump.net/get.php?id={uuidOfWindowsVersion}&lang={lowerLang}");
  406. switch (responseString.StatusCode)
  407. {
  408. //200 Status code
  409. case HttpStatusCode.OK:
  410. {
  411. var result = responseString.Content.ReadAsStringAsync().Result;
  412. dynamic data = JObject.Parse(result);
  413. //Add different urls to different packages to a list
  414. var urls = new Dictionary<string, string>
  415. {
  416. {
  417. "basic", (string) data["response"]["files"][
  418. $"microsoft-windows-languagefeatures-basic-{lowerLang}-package-{convertedArch}.cab"]
  419. [
  420. "url"]
  421. },
  422. {
  423. "hw", (string) data["response"]["files"][
  424. $"microsoft-windows-languagefeatures-handwriting-{lowerLang}-package-{convertedArch}.cab"]
  425. [
  426. "url"]
  427. },
  428. {
  429. "ocr", (string) data["response"]["files"][
  430. $"microsoft-windows-languagefeatures-ocr-{lowerLang}-package-{convertedArch}.cab"][
  431. "url"]
  432. },
  433. {
  434. "speech", (string) data["response"]["files"][
  435. $"microsoft-windows-languagefeatures-speech-{lowerLang}-package-{convertedArch}.cab"]
  436. [
  437. "url"]
  438. },
  439. {
  440. "tts", (string) data["response"]["files"][
  441. $"microsoft-windows-languagefeatures-texttospeech-{lowerLang}-package-{convertedArch}.cab"]
  442. [
  443. "url"]
  444. }
  445. };
  446. var amePath = Path.Combine(Path.GetTempPath(), "AME\\");
  447. //Create the directory if it doesn't exist.
  448. var file = new FileInfo(amePath);
  449. file.Directory?.Create(); //Does nothing if the directory already exists
  450. //Final result being "temp\AME\Languages\file.cab"
  451. var downloadPath = Path.Combine(amePath, "Languages\\");
  452. file = new FileInfo(downloadPath);
  453. file.Directory?.Create();
  454. using (var webClient = new WebClient())
  455. {
  456. Console.WriteLine($"Downloading {lowerLang}.cab file, please wait..");
  457. foreach (var url in urls)
  458. {
  459. //Check if the file exists, if it does exist, skip it.
  460. if (File.Exists(Path.Combine(downloadPath, $"{url.Key}_{lowerLang}.cab")))
  461. {
  462. Console.WriteLine($"{url.Key}_{lowerLang} already exists, skipping.");
  463. continue;
  464. }
  465. //Output file format: featureName_languageCode.cab: speech_de-de.cab
  466. webClient.DownloadFile(url.Value, $@"{downloadPath}\{url.Key}_{lowerLang}.cab");
  467. }
  468. }
  469. break;
  470. }
  471. //400 Status code
  472. case HttpStatusCode.BadRequest:
  473. {
  474. var result = responseString.Content.ReadAsStringAsync().Result;
  475. dynamic data = JObject.Parse(result);
  476. Console.WriteLine($"Bad request.\r\nError:{data["response"]["error"]}");
  477. break;
  478. }
  479. //429 Status code
  480. case (HttpStatusCode)429:
  481. {
  482. var result = responseString.Content.ReadAsStringAsync().Result;
  483. dynamic data = JObject.Parse(result);
  484. Console.WriteLine($"Too many requests, try again later.\r\nError:{data["response"]["error"]}");
  485. break;
  486. }
  487. //500 Status code
  488. case HttpStatusCode.InternalServerError:
  489. {
  490. var result = responseString.Content.ReadAsStringAsync().Result;
  491. dynamic data = JObject.Parse(result);
  492. Console.WriteLine($"Internal Server Error.\r\nError:{data["response"]["error"]}");
  493. break;
  494. }
  495. default:
  496. throw new ArgumentOutOfRangeException();
  497. }
  498. }
  499. }
  500. public static async Task<bool> SafeRunAction(ITaskAction action)
  501. {
  502. try
  503. {
  504. return await action.RunTask();
  505. }
  506. catch (Exception e)
  507. {
  508. action.ResetProgress();
  509. if (e.InnerException != null)
  510. {
  511. ErrorLogger.WriteToErrorLog(e.InnerException.Message, e.InnerException.StackTrace, e.Message);
  512. }
  513. else
  514. {
  515. ErrorLogger.WriteToErrorLog(e.Message, e.StackTrace, action.ErrorString());
  516. }
  517. }
  518. return false;
  519. }
  520. }
  521. }