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.

589 lines
26 KiB

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