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

using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.MemoryMappedFiles;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Runtime.InteropServices;
using System.ServiceProcess;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Xml;
using System.Xml.Serialization;
using TrustedUninstaller.Shared.Actions;
using TrustedUninstaller.Shared.Parser;
using TrustedUninstaller.Shared.Tasks;
namespace TrustedUninstaller.Shared
{
public static class AmeliorationUtil
{
private static readonly ConfigParser Parser = new ConfigParser();
private static readonly HttpClient Client = new HttpClient();
public static Playbook Playbook { set; get; }
public static readonly List<string> ErrorDisplayList = new List<string>();
public static int GetProgressMaximum()
{
return Parser.Tasks.Sum(task => task.Actions.Sum(action => action.GetProgressWeight()));
}
public static bool AddTasks(string configPath, string file)
{
try
{
//This allows for a proper detection of if any error occurred, and if so the CLI will relay an :AME-Fatal Error:
//This is important, as we want the process to stop immediately if a YAML syntax error was detected.
bool hadError = false;
//Adds the config file to the parser's task list
Parser.Add(Path.Combine(configPath, file));
var currentTask = Parser.Tasks[Parser.Tasks.Count - 1];
if (File.Exists("TasksAdded.txt"))
{
var doneTasks = File.ReadAllText("TasksAdded.txt").Split(new[] { "\r\n" }, StringSplitOptions.None);
if (doneTasks.Contains(currentTask.Title))
{
Parser.Tasks.Remove(currentTask);
return true;
}
}
//Get the features of the last added task (the task that was just added from the config file)
var features = currentTask.Features;
//Each feature would reference a directory that has a YAML file, we take those directories and then run the
//AddTasks function again, until we reach a file that doesn't reference any other YAML files, and add them
//all to the parser's tasks list.
if (features == null) return true;
foreach (var feature in features)
{
var subResult = AddTasks(configPath, feature);
// We could return false here, however we want to output ALL detected YAML errors,
// which is why we continue here.
if (!subResult) hadError = true;
}
return hadError ? false : true;
}
catch (Exception e)
{
Console.WriteLine($"Error adding tasks in {configPath + "\\" + file}:\r\n{e.Message}");
ErrorLogger.WriteToErrorLog(e.Message, e.StackTrace, $"Error adding tasks in {configPath + "\\" + file}.");
return false;
}
}
public static async Task<int> DoActions(UninstallTask task, UninstallTaskPrivilege privilege)
{
try
{
//If the privilege is admin and the program is running as TI, do not do the action.
if (privilege == UninstallTaskPrivilege.Admin && WinUtil.IsTrustedInstaller())
{
return 0;
}
if (privilege == UninstallTaskPrivilege.TrustedInstaller && !WinUtil.IsTrustedInstaller())
{
Console.WriteLine("Relaunching as Trusted Installer!");
var mmf = MemoryMappedFile.CreateNew("ImgA", 5000000);
WinUtil.RelaunchAsTrustedInstaller();
if (NativeProcess.Process == null)
{
ErrorLogger.WriteToErrorLog($"Could not launch TrustedInstaller process. Return output was null.",
Environment.StackTrace, "Error while attempting to sync with TrustedInstaller process.");
Console.WriteLine(":AME-Fatal Error: Could not launch TrustedInstaller process.");
Environment.Exit(-1);
}
var delay = 20;
while (!NativeProcess.Process.HasExited)
{
if (delay > 3500)
{
NativeProcess.Process.Kill();
ErrorLogger.WriteToErrorLog($"Could not initialize memory data exchange. Timeframe exceeded.",
Environment.StackTrace, "Error while attempting to sync with TrustedInstaller process.");
Console.WriteLine(":AME-Fatal Error: Could not initialize memory data exchange.");
Environment.Exit(-1);
}
Task.Delay(delay).Wait();
// Kind of inefficient looping this, however it's likely to cause access errors otherwise
using var stream = mmf.CreateViewStream();
using BinaryReader binReader = new BinaryReader(stream);
{
var res = binReader.ReadBytes((int)stream.Length);
var data = Encoding.UTF8.GetString(res);
var end = data.IndexOf('\0');
if (end == 0)
{
delay += 200;
}
else
{
break;
}
}
}
var offset = 0;
var read = false;
using (var stream = mmf.CreateViewStream())
{
while (!NativeProcess.Process.HasExited || read)
{
read = false;
BinaryReader binReader = new BinaryReader(stream);
binReader.BaseStream.Seek(offset, SeekOrigin.Begin);
var res = binReader.ReadBytes((int)stream.Length - offset);
var data = Encoding.UTF8.GetString(res);
var end = data.IndexOf("\0");
var content = data.Substring(0, end);
offset += Encoding.UTF8.GetBytes(content).Length;
var output = content.Split(new [] {Environment.NewLine}, StringSplitOptions.None);
if (output.Length > 0) output = output.Take(output.Length - 1).ToArray();
foreach (var line in output)
{
Console.WriteLine(line);
read = true;
// Introducing ANY delay here makes it lag behind, which isn't ideal
//Task.Delay(5).Wait();
}
Task.Delay(20).Wait();
}
}
mmf.Dispose();
return 0; //Only returns after TI is done
}
//Goes through the list of tasks that are inside the parser class,
//and runs the task using the RunTask method
//Check the Actions folder inside the Shared folder for reference.
foreach (ITaskAction action in task.Actions)
{
int i = 0;
//var actionType = action.GetType().ToString().Replace("TrustedUninstaller.Shared.Actions.", "");
try
{
do
{
//Console.WriteLine($"Running {actionType}");
Console.WriteLine();
try
{
await action.RunTask();
action.ResetProgress();
}
catch (Exception e)
{
action.ResetProgress();
if (e.InnerException != null)
{
ErrorLogger.WriteToErrorLog(e.InnerException.Message, e.InnerException.StackTrace, e.Message);
}
else
{
ErrorLogger.WriteToErrorLog(e.Message, e.StackTrace, action.ErrorString());
List<string> ExceptionBreakList = new List<string>() { "System.ArgumentException", "System.SecurityException", "System.UnauthorizedAccessException", "System.UnauthorizedAccessException", "System.TimeoutException" };
if (ExceptionBreakList.Any(x => x.Equals(e.GetType().ToString())))
{
i = 10;
break;
}
}
}
Console.WriteLine($"Status: {action.GetStatus()}");
if (i > 0) Thread.Sleep(50);
i++;
} while (action.GetStatus() != UninstallTaskStatus.Completed && i < 10);
}
catch (Exception e)
{
ErrorLogger.WriteToErrorLog(e.Message, e.StackTrace, "Critical error while running action.");
Console.WriteLine($":AME-ERROR: Critical error while running action: " + e.Message);
}
if (i == 10)
{
var errorString = action.ErrorString();
ErrorLogger.WriteToErrorLog(errorString, Environment.StackTrace, "Action failed to complete.");
// AmeliorationUtil.ErrorDisplayList.Add(errorString) would NOT work here since this
// might be a separate process, and thus has to be forwarded via the console
Console.WriteLine($":AME-ERROR: {errorString}");
//Environment.Exit(-2);
Console.WriteLine($"Action completed. Weight:{action.GetProgressWeight()}");
continue;
}
Console.WriteLine($"Action completed. Weight:{action.GetProgressWeight()}");
}
Console.WriteLine("Task completed.");
File.AppendAllText("TasksAdded.txt", task.Title + Environment.NewLine);
}
catch (Exception e)
{
ErrorLogger.WriteToErrorLog(e.Message, e.StackTrace,
"Encountered an error while doing task actions.");
}
return 0;
}
public static Task<Playbook> DeserializePlaybook(string dir)
{
Playbook pb;
XmlSerializer serializer = new XmlSerializer(typeof(Playbook));
using (XmlReader reader = XmlReader.Create($"{dir}\\playbook.conf"))
{
pb = (Playbook)serializer.Deserialize(reader);
}
pb.Path = dir;
return Task.FromResult(pb);
}
public static async Task<int> StartAmelioration()
{
//Needed after defender removal's reboot, the "current directory" will be set to System32
//After the auto start up.
Directory.SetCurrentDirectory(AppDomain.CurrentDomain.BaseDirectory);
if (File.Exists("TasksAdded.txt") && !WinUtil.IsTrustedInstaller())
{
File.Delete("TasksAdded.txt");
}
if (Directory.Exists("Logs") && !WinUtil.IsTrustedInstaller())
{
if (File.Exists("Logs\\AdminOutput.txt"))
{
File.Delete("Logs\\AdminOutput.txt");
}
if (File.Exists("Logs\\TIOutput.txt"))
{
File.Delete("Logs\\TIOutput.txt");
}
if (File.Exists("Logs\\FileChecklist.txt"))
{
File.Delete("Logs\\FileChecklist.txt");
}
}
//Check if KPH is installed.
ServiceController service = ServiceController.GetDevices()
.FirstOrDefault(s => s.DisplayName == "KProcessHacker2");
if (service == null)
{
//Installs KPH
await WinUtil.RemoveProtectionAsync();
}
var langsFile = Path.Combine($"{Playbook.Path}\\Configuration", "langs.txt");
//Download language packs that were selected by the user
if (!File.Exists(langsFile))
{
File.Create(langsFile);
}
//var langsSelected = File.ReadLines(langsFile);
//await DownloadLanguagesAsync(langsSelected);
//Start adding tasks from the top level configuration folder.
if (!AddTasks($"{Playbook.Path}\\Configuration", "custom.yml"))
{
Console.WriteLine($":AME-Fatal Error: Error adding tasks.");
Environment.Exit(1);
}
if (!Parser.Tasks.Any())
{
Console.Error.WriteLine($"Couldn't find any tasks.");
return -1;
}
//Sort the list based on the priority value.
if (Parser.Tasks.Any(x => x.Priority != Parser.Tasks.First().Priority))
Parser.Tasks.Sort(new TaskComparer());
UninstallTaskPrivilege prevPriv = UninstallTaskPrivilege.Admin;
foreach (var task in Parser.Tasks.Where(task => task.Actions.Count != 0))
{
try
{
if (prevPriv == UninstallTaskPrivilege.TrustedInstaller && task.Privilege == UninstallTaskPrivilege.TrustedInstaller && !WinUtil.IsTrustedInstaller())
{
continue;
}
await DoActions(task, task.Privilege);
prevPriv = task.Privilege;
}
catch (Exception ex)
{
ErrorLogger.WriteToErrorLog(ex.Message, ex.StackTrace, "Error during DoAction loop.");
}
}
if (WinUtil.IsTrustedInstaller()) return 0;
WinUtil.RegistryManager.UnhookUserHives();
//Check how many files were successfully and unsuccessfully deleted.
var deletedItemsCount = 0;
var failedDeletedItemsCount = 0;
if (File.Exists("Logs\\FileChecklist.txt"))
{
using (var reader = new StreamReader("Logs\\FileChecklist.txt"))
{
var data = reader.ReadToEnd();
var listData = data.Split(new [] { Environment.NewLine }, StringSplitOptions.None).ToList();
deletedItemsCount = listData.FindAll(s => s == "Deleted: True").Count();
failedDeletedItemsCount = listData.FindAll(s => s == "Deleted: False").Count();
}
using (var writer = new StreamWriter("Logs\\FileChecklist.txt", true))
{
writer.WriteLine($"{deletedItemsCount} files were deleted successfully. " +
$"{failedDeletedItemsCount} files couldn't be deleted.");
}
}
Console.WriteLine($"{deletedItemsCount} files were deleted successfully. " +
$"{failedDeletedItemsCount} files couldn't be deleted.");
//Check if the kernel driver is installed.
//service = ServiceController.GetDevices()
//.FirstOrDefault(s => s.DisplayName == "KProcessHacker2");
if (true)
{
//Remove Process Hacker's kernel driver.
await WinUtil.UninstallDriver();
}
await AmeliorationUtil.SafeRunAction(new RegistryKeyAction()
{
KeyName = @"HKLM\SYSTEM\CurrentControlSet\Services\KProcessHacker2",
});
File.Delete("TasksAdded.txt");
Console.WriteLine();
Console.WriteLine("Playbook finished.");
return 0;
}
public static async Task DownloadLanguagesAsync(IEnumerable<string> langsSelected)
{
foreach (var lang in langsSelected)
{
var lowerLang = lang.ToLower();
var arch = RuntimeInformation.OSArchitecture;
var winVersion = Environment.OSVersion.Version.Build;
var convertedArch = "";
switch (arch)
{
case Architecture.X64:
convertedArch = "amd64";
break;
case Architecture.Arm64:
convertedArch = "arm64";
break;
case Architecture.X86:
convertedArch = "x86";
break;
}
var uuidOfWindowsVersion = "";
var uuidResponse =
await Client.GetAsync(
$"https://api.uupdump.net/listid.php?search={winVersion}%20{convertedArch}&sortByDate=1");
switch (uuidResponse.StatusCode)
{
//200 Status code
case HttpStatusCode.OK:
{
var result = uuidResponse.Content.ReadAsStringAsync().Result;
//Gets the UUID of the first build object in the response, we take the first since it's the newest.
uuidOfWindowsVersion = (string)(JToken.Parse(result)["response"]?["builds"]?.Children().First()
.Children().First().Last());
break;
}
//400 Status code
case HttpStatusCode.BadRequest:
{
var result = uuidResponse.Content.ReadAsStringAsync().Result;
dynamic data = JObject.Parse(result);
Console.WriteLine($"Bad request.\r\nError:{data["response"]["error"]}");
break;
}
//429 Status code
case (HttpStatusCode)429:
{
var result = uuidResponse.Content.ReadAsStringAsync().Result;
dynamic data = JObject.Parse(result);
Console.WriteLine($"Too many requests, try again later.\r\nError:{data["response"]["error"]}");
break;
}
//500 Status code
case HttpStatusCode.InternalServerError:
{
var result = uuidResponse.Content.ReadAsStringAsync().Result;
dynamic data = JObject.Parse(result);
Console.WriteLine($"Internal Server Error.\r\nError:{data["response"]["error"]}");
break;
}
default:
throw new ArgumentOutOfRangeException();
}
var responseString =
await Client.GetAsync(
$"https://api.uupdump.net/get.php?id={uuidOfWindowsVersion}&lang={lowerLang}");
switch (responseString.StatusCode)
{
//200 Status code
case HttpStatusCode.OK:
{
var result = responseString.Content.ReadAsStringAsync().Result;
dynamic data = JObject.Parse(result);
//Add different urls to different packages to a list
var urls = new Dictionary<string, string>
{
{
"basic", (string) data["response"]["files"][
$"microsoft-windows-languagefeatures-basic-{lowerLang}-package-{convertedArch}.cab"]
[
"url"]
},
{
"hw", (string) data["response"]["files"][
$"microsoft-windows-languagefeatures-handwriting-{lowerLang}-package-{convertedArch}.cab"]
[
"url"]
},
{
"ocr", (string) data["response"]["files"][
$"microsoft-windows-languagefeatures-ocr-{lowerLang}-package-{convertedArch}.cab"][
"url"]
},
{
"speech", (string) data["response"]["files"][
$"microsoft-windows-languagefeatures-speech-{lowerLang}-package-{convertedArch}.cab"]
[
"url"]
},
{
"tts", (string) data["response"]["files"][
$"microsoft-windows-languagefeatures-texttospeech-{lowerLang}-package-{convertedArch}.cab"]
[
"url"]
}
};
var amePath = Path.Combine(Path.GetTempPath(), "AME\\");
//Create the directory if it doesn't exist.
var file = new FileInfo(amePath);
file.Directory?.Create(); //Does nothing if the directory already exists
//Final result being "temp\AME\Languages\file.cab"
var downloadPath = Path.Combine(amePath, "Languages\\");
file = new FileInfo(downloadPath);
file.Directory?.Create();
using (var webClient = new WebClient())
{
Console.WriteLine($"Downloading {lowerLang}.cab file, please wait..");
foreach (var url in urls)
{
//Check if the file exists, if it does exist, skip it.
if (File.Exists(Path.Combine(downloadPath, $"{url.Key}_{lowerLang}.cab")))
{
Console.WriteLine($"{url.Key}_{lowerLang} already exists, skipping.");
continue;
}
//Output file format: featureName_languageCode.cab: speech_de-de.cab
webClient.DownloadFile(url.Value, $@"{downloadPath}\{url.Key}_{lowerLang}.cab");
}
}
break;
}
//400 Status code
case HttpStatusCode.BadRequest:
{
var result = responseString.Content.ReadAsStringAsync().Result;
dynamic data = JObject.Parse(result);
Console.WriteLine($"Bad request.\r\nError:{data["response"]["error"]}");
break;
}
//429 Status code
case (HttpStatusCode)429:
{
var result = responseString.Content.ReadAsStringAsync().Result;
dynamic data = JObject.Parse(result);
Console.WriteLine($"Too many requests, try again later.\r\nError:{data["response"]["error"]}");
break;
}
//500 Status code
case HttpStatusCode.InternalServerError:
{
var result = responseString.Content.ReadAsStringAsync().Result;
dynamic data = JObject.Parse(result);
Console.WriteLine($"Internal Server Error.\r\nError:{data["response"]["error"]}");
break;
}
default:
throw new ArgumentOutOfRangeException();
}
}
}
public static async Task<bool> SafeRunAction(ITaskAction action)
{
try
{
return await action.RunTask();
}
catch (Exception e)
{
action.ResetProgress();
if (e.InnerException != null)
{
ErrorLogger.WriteToErrorLog(e.InnerException.Message, e.InnerException.StackTrace, e.Message);
}
else
{
ErrorLogger.WriteToErrorLog(e.Message, e.StackTrace, action.ErrorString());
}
}
return false;
}
}
}