diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..13840d1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +packages/ +.vs/ +source/KO-TBLCryptoEditor/obj/ +source/KO-TBLCryptoEditor/bin/ +UpgradeLog.htm +source/KO-TBLCryptoEditor/FodyWeavers.xml +source/KO-TBLCryptoEditor/FodyWeavers.xsd diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..572853e --- /dev/null +++ b/.gitmodules @@ -0,0 +1,6 @@ +[submodule "thirdparty/PatternFinder"] + path = thirdparty/PatternFinder + url = https://github.com/ko4life-net/PatternFinder.git +[submodule "thirdparty/pe"] + path = thirdparty/pe + url = https://github.com/ko4life-net/pe.git diff --git a/KO-TBLCryptoEditor.sln b/KO-TBLCryptoEditor.sln new file mode 100644 index 0000000..c5f8c03 --- /dev/null +++ b/KO-TBLCryptoEditor.sln @@ -0,0 +1,50 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.30104.148 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KO-TBLCryptoEditor", "source\KO-TBLCryptoEditor\KO-TBLCryptoEditor.csproj", "{29854838-9FCA-4090-BA6E-85CD54280847}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "thirdparty", "thirdparty", "{1790BEB3-7281-48E1-B189-4EADB596D905}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PatternFinder", "thirdparty\PatternFinder\PatternFinder\PatternFinder.csproj", "{5FFB2393-5455-49C1-8D45-3EA5C0F22386}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Workshell.PE", "thirdparty\pe\src\Workshell.PE\Workshell.PE.csproj", "{A08F8A40-9EEA-4A5D-B434-C4FBFD5EFFD8}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + CI|Any CPU = CI|Any CPU + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {29854838-9FCA-4090-BA6E-85CD54280847}.CI|Any CPU.ActiveCfg = Release|Any CPU + {29854838-9FCA-4090-BA6E-85CD54280847}.CI|Any CPU.Build.0 = Release|Any CPU + {29854838-9FCA-4090-BA6E-85CD54280847}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {29854838-9FCA-4090-BA6E-85CD54280847}.Debug|Any CPU.Build.0 = Debug|Any CPU + {29854838-9FCA-4090-BA6E-85CD54280847}.Release|Any CPU.ActiveCfg = Release|Any CPU + {29854838-9FCA-4090-BA6E-85CD54280847}.Release|Any CPU.Build.0 = Release|Any CPU + {5FFB2393-5455-49C1-8D45-3EA5C0F22386}.CI|Any CPU.ActiveCfg = Release|Any CPU + {5FFB2393-5455-49C1-8D45-3EA5C0F22386}.CI|Any CPU.Build.0 = Release|Any CPU + {5FFB2393-5455-49C1-8D45-3EA5C0F22386}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5FFB2393-5455-49C1-8D45-3EA5C0F22386}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5FFB2393-5455-49C1-8D45-3EA5C0F22386}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5FFB2393-5455-49C1-8D45-3EA5C0F22386}.Release|Any CPU.Build.0 = Release|Any CPU + {A08F8A40-9EEA-4A5D-B434-C4FBFD5EFFD8}.CI|Any CPU.ActiveCfg = CI|Any CPU + {A08F8A40-9EEA-4A5D-B434-C4FBFD5EFFD8}.CI|Any CPU.Build.0 = CI|Any CPU + {A08F8A40-9EEA-4A5D-B434-C4FBFD5EFFD8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A08F8A40-9EEA-4A5D-B434-C4FBFD5EFFD8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A08F8A40-9EEA-4A5D-B434-C4FBFD5EFFD8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A08F8A40-9EEA-4A5D-B434-C4FBFD5EFFD8}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {5FFB2393-5455-49C1-8D45-3EA5C0F22386} = {1790BEB3-7281-48E1-B189-4EADB596D905} + {A08F8A40-9EEA-4A5D-B434-C4FBFD5EFFD8} = {1790BEB3-7281-48E1-B189-4EADB596D905} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {58EDDF0E-2C5B-4255-B74B-E468E971CD4F} + EndGlobalSection +EndGlobal diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..5536ba3 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 ko4life.net + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..da11c0e --- /dev/null +++ b/README.md @@ -0,0 +1,39 @@ +# Knight Online Table Crypto Editor + +This tool will allow targeting any version KnightOnLine.exe and change/update continuously their encryption keys. If you are looking into cheats, you're probably in the wrong place, as this tool is meant to be used for people that are into KO development. + +Loading the target executable into the tool will parse all the bytes in the binary that have similar pattern for the encryption algorithm, so when it comes to patching, it knows exactly the right offsets to be patched. + +For more information, please visit [ko4life.net](https://www.ko4life.net/). + +### Getting Started + +You can decide whether you want to build this project from source or download the binary from the Releases [here](https://github.com/ko4life-net/KO-TBLCryptoEditor/releases). + +It is recommended that the target exe you load into this tool, will be in the root directory where the client is, so it would automatically detect the Data folder where the tbls are and update their encryption to match with the new keys as well. + +Note that if you cannot update Key2, it is because the parser detected that the compiler inlined some of the functions, which made the compiler generate multiple instructions to create the second key dynamically. Therefore, it would only be enabled for executables that do not have inlined functions. + +After generating new random keys and clicking the `Update Client Encryption`, make sure the next step you do, is update your TBLs as well by clicking `Update Data Encryption`. + +### Preview + +![](/media/main_window.png) + +You can also view detailed report of the found offsets and target executeable: + +![](/media/view_offsets_window.png) + +Also note that it detects the encryption algorithm used for the tbls: + +![](/media/view_offsets_window2.png) + +## Contributing + +Pull-Requests are greatly appreciated should you like to contribute to the project. + +Same goes for opening issues; if you have any suggestions, feedback or you found any bugs, please do not hesitate to open an [issue](https://github.com/ko4life-net/KO-TBLCryptoEditor/issues) or reach out at [ko4life.net](https://www.ko4life.net/). + +## License + +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. diff --git a/docs/LICENSE b/docs/LICENSE new file mode 100644 index 0000000..5536ba3 --- /dev/null +++ b/docs/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 ko4life.net + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/media/main_window.png b/media/main_window.png new file mode 100644 index 0000000..bc825b3 Binary files /dev/null and b/media/main_window.png differ diff --git a/media/view_offsets_window.png b/media/view_offsets_window.png new file mode 100644 index 0000000..4267e26 Binary files /dev/null and b/media/view_offsets_window.png differ diff --git a/media/view_offsets_window2.png b/media/view_offsets_window2.png new file mode 100644 index 0000000..abc964b Binary files /dev/null and b/media/view_offsets_window2.png differ diff --git a/source/KO-TBLCryptoEditor/App.config b/source/KO-TBLCryptoEditor/App.config new file mode 100644 index 0000000..209431a --- /dev/null +++ b/source/KO-TBLCryptoEditor/App.config @@ -0,0 +1,11 @@ + + + + + + + + + + diff --git a/source/KO-TBLCryptoEditor/Controls/CryptoKeyInput.cs b/source/KO-TBLCryptoEditor/Controls/CryptoKeyInput.cs new file mode 100644 index 0000000..ab25bde --- /dev/null +++ b/source/KO-TBLCryptoEditor/Controls/CryptoKeyInput.cs @@ -0,0 +1,57 @@ +using System; +using System.Drawing; +using System.Globalization; +using System.Windows.Forms; + +namespace KO.TBLCryptoEditor.Controls +{ + public class CryptoKeyInput : TextBox + { + public short Key => Convert.ToInt16(Text, 16); + + public CryptoKeyInput() + { + TextChanged += delegate { Validate(); }; + } + + private void Validate() + { + Action cartToEnd = () => + { + SelectionStart = Text.Length; + SelectionLength = 0; + }; + + if (!Text.StartsWith("0x")) + { + Text = "0x"; + cartToEnd(); + return; + } + + if (Text.Length > 6) + { + Text = Text.Substring(0, 6); + cartToEnd(); + return; + } + + if (Text.Length > 2) + { + if (!Int32.TryParse(Text.Substring(2, Text.Length - 2), NumberStyles.HexNumber, + CultureInfo.CurrentCulture, out int _)) + { + Text = "0x"; + BackColor = Color.Crimson; + cartToEnd(); + return; + } + } + + + Text = "0x" + Text.Substring(2, Text.Length - 2).ToUpper(); + BackColor = Text.Length != 6 ? Color.Crimson : SystemColors.Control; + cartToEnd(); + } + } +} diff --git a/source/KO-TBLCryptoEditor/Core/CryptoKey.cs b/source/KO-TBLCryptoEditor/Core/CryptoKey.cs new file mode 100644 index 0000000..2d2036a --- /dev/null +++ b/source/KO-TBLCryptoEditor/Core/CryptoKey.cs @@ -0,0 +1,39 @@ +using System; + +namespace KO.TBLCryptoEditor.Core +{ + public class CryptoKey + { + public long Offset { get; } + public ushort Key { get; } + public long VirtualAddress { get; } + + public CryptoKey(long offset, ushort key, long virtualAdderss) + { + Offset = offset; + Key = key; + VirtualAddress = virtualAdderss; + } + + public override bool Equals(object obj) + { + CryptoKey key = (CryptoKey)obj; + return key.Offset == Offset + && key.Key == Key + && key.VirtualAddress == VirtualAddress; + } + + public override int GetHashCode() + { + unchecked + { + return (Offset.GetHashCode() * 397) ^ Key.GetHashCode(); + } + } + + public override string ToString() + { + return $"[0x{(int)Offset:X8}||0x{(int)VirtualAddress:X8}]: 0x{Key:X4}"; + } + } +} diff --git a/source/KO-TBLCryptoEditor/Core/CryptoPatch.cs b/source/KO-TBLCryptoEditor/Core/CryptoPatch.cs new file mode 100644 index 0000000..bb63a5b --- /dev/null +++ b/source/KO-TBLCryptoEditor/Core/CryptoPatch.cs @@ -0,0 +1,70 @@ +using System; +using System.Collections; +using System.Collections.Generic; + +namespace KO.TBLCryptoEditor.Core +{ + public class CryptoPatch : IEnumerable + { + public const int KEYS_COUNT = 3; + + public long RegionOffset { get; } + public long RegionVA { get; } + public CryptoKey[] Keys { get; set; } + public bool Inlined { get; set; } + + + public CryptoPatch(long regionOffset, long regionVA, CryptoKey[] keys = null, bool inlined = false) + { + if (keys != null && keys.Length != KEYS_COUNT) + throw new ArgumentException("Mismatched keys length."); + + RegionOffset = regionOffset; + RegionVA = regionVA; + Keys = keys ?? new CryptoKey[KEYS_COUNT]; + Inlined = inlined; + } + + public CryptoKey this[int index] + { + get + { + if (index > KEYS_COUNT - 1) + throw new IndexOutOfRangeException(); + + return Keys[index]; + } + set + { + if (index > KEYS_COUNT - 1) + throw new IndexOutOfRangeException(); + + Keys[index] = value ?? throw new ArgumentNullException(); + } + } + + public IEnumerator GetEnumerator() + { + foreach (var key in Keys) + yield return key; + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public override string ToString() + { + return base.ToString(); + } + + public override int GetHashCode() + { + unchecked + { + return (RegionOffset.GetHashCode() * 397) ^ RegionVA.GetHashCode(); + } + } + } +} diff --git a/source/KO-TBLCryptoEditor/Core/CryptoPatchContainer.cs b/source/KO-TBLCryptoEditor/Core/CryptoPatchContainer.cs new file mode 100644 index 0000000..ed2c110 --- /dev/null +++ b/source/KO-TBLCryptoEditor/Core/CryptoPatchContainer.cs @@ -0,0 +1,128 @@ +using System; +using System.Collections; +using System.Collections.Generic; + +namespace KO.TBLCryptoEditor.Core +{ + public class CryptoPatchContainer : IEnumerable, ICollection + { + public CryptoType CryptoType { get; set; } + public bool Inlined { get; set; } + public int Count => _patches.Count; + public int Capacity + { + get => _patches.Count; + set => _patches.Capacity = value; + } + public bool IsReadOnly => false; + + private List _patches; + + public CryptoPatchContainer() + { + CryptoType = CryptoType.UNKNOWN; + Inlined = false; + _patches = new List(); + } + + public CryptoPatch this[int index] + { + get + { + if (index >= _patches.Count) + throw new IndexOutOfRangeException(); + + return _patches[index]; + } + set + { + if (index >= _patches.Count) + throw new IndexOutOfRangeException(); + + _patches[index] = value ?? throw new ArgumentNullException(); + } + } + + public void Add(CryptoPatch patch) + { + if (patch == null) + throw new ArgumentNullException(); + + _patches.Add(patch); + } + + public bool Remove(CryptoPatch patch) + { + if (patch == null) + throw new ArgumentNullException(); + + return _patches.Remove(patch); + } + + public CryptoPatch Find(CryptoPatch patch) + { + if (patch == null) + throw new ArgumentNullException(); + + return _patches.Find(p => p == patch); + } + + public CryptoPatch Find(Predicate match) + { + if (match == null) + throw new ArgumentNullException(); + + for (int i = 0; i < Count; i++) + if (match(_patches[i])) + return _patches[i]; + + return default; + } + + public List FindAll(Predicate match) + { + if (match == null) + throw new ArgumentNullException(); + + List patches = new List(); + for (int i = 0; i < Count; i++) + if (match(_patches[i])) + patches.Add(_patches[i]); + + return patches; + } + + public IEnumerator GetEnumerator() + { + foreach (var patch in _patches) + yield return patch; + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public void Clear() + { + _patches.Clear(); + } + + public bool Contains(CryptoPatch patch) + { + return _patches.Contains(patch); + } + + public void CopyTo(CryptoPatch[] array, int arrayIndex) + { + if (array == null) + throw new ArgumentNullException("array"); + + CryptoPatch[] ppArray = array as CryptoPatch[]; + if (ppArray == null) + throw new ArgumentException(); + + ((ICollection)this).CopyTo(ppArray, arrayIndex); + } + } +} diff --git a/source/KO-TBLCryptoEditor/Core/FileSecurity.cs b/source/KO-TBLCryptoEditor/Core/FileSecurity.cs new file mode 100644 index 0000000..6aa7bac --- /dev/null +++ b/source/KO-TBLCryptoEditor/Core/FileSecurity.cs @@ -0,0 +1,51 @@ +using System; + +namespace KO.TBLCryptoEditor.Core +{ + /// + /// Used for batch table conversion, so we know what kind of encryption the target uses. + /// DES won't be supported for now, to reduce impact on the official game, so cheaters + /// won't copy this code to their bots for w/e reason. + /// + public enum CryptoType + { + /// + /// Simple XOR cipher encryption. + /// + XOR, + + /// + /// DES comes with some salt ;) + /// and then regular XOR cipher. + /// + DES_XOR, + + /// + /// :) + /// + UNKNOWN + } + + public class FileSecurity + { + public static void EncryptXOR(byte[] data, short key_r, short key_c1, short key_c2) + { + for (int i = 0; i < data.Length; i++) + { + byte byData = (byte)(data[i] ^ (key_r >> 8)); + key_r = (short)((byData + key_r) * key_c1 + key_c2); + data[i] = byData; + } + } + + public static void DecryptXOR(byte[] data, short key_r, short key_c1, short key_c2) + { + for (int i = 0; i < data.Length; i++) + { + byte byData = (byte)(data[i] ^ (key_r >> 8)); + key_r = (short)((data[i] + key_r) * key_c1 + key_c2); + data[i] = byData; + } + } + } +} diff --git a/source/KO-TBLCryptoEditor/Core/InitializationFailure.cs b/source/KO-TBLCryptoEditor/Core/InitializationFailure.cs new file mode 100644 index 0000000..c2ae626 --- /dev/null +++ b/source/KO-TBLCryptoEditor/Core/InitializationFailure.cs @@ -0,0 +1,14 @@ +using System; + +namespace KO.TBLCryptoEditor.Core +{ + public class InitializationFailure + { + public string Reason { get; } + + public InitializationFailure(string reason) + { + Reason = reason; + } + } +} diff --git a/source/KO-TBLCryptoEditor/Core/TargetPE.cs b/source/KO-TBLCryptoEditor/Core/TargetPE.cs new file mode 100644 index 0000000..1fcd3f1 --- /dev/null +++ b/source/KO-TBLCryptoEditor/Core/TargetPE.cs @@ -0,0 +1,441 @@ +using System; +using System.IO; +using System.Linq; +using System.Collections.Generic; + +using Workshell.PE; +using PatternFinder; + +using KO.TBLCryptoEditor.Utils; + +namespace KO.TBLCryptoEditor.Core +{ + public partial class TargetPE + { + private byte[] _data; + + public PortableExecutableImage PE { get; } + public LocationCalculator Calculator { get; } + public FileInfo FileInfo { get; } + public string FilePath { get; } + public int MajorLinkerVersion { get; private set; } + public long BaseAddress { get; private set; } + public CryptoPatchContainer CryptoPatches { get; private set; } + public int ClientVersion { get; private set; } + public bool IsValid { get; private set; } + public bool CanUpdateKey2 { get; private set; } + + + public TargetPE(string filePath) + { + FileInfo fi = new FileInfo(filePath); + if (fi.Exists) + { + _data = GetFileBytes(fi.FullName); + if (_data == null) + throw new FileLoadException("The target file seem to be locked."); + + PE = PortableExecutableImage.FromBytes(_data); + if (PE != null && PE.Is32Bit) + { + Calculator = PE.GetCalculator(); + + FilePath = fi.FullName; + FileInfo = fi; + + BaseAddress = (long)PE.NTHeaders.OptionalHeader.ImageBase; + MajorLinkerVersion = PE.NTHeaders.OptionalHeader.MajorLinkerVersion; + + CryptoPatches = new CryptoPatchContainer(); + ClientVersion = -1; + + IsValid = true; + CanUpdateKey2 = true; + } + } + } + + public InitializationFailure Initialize(bool skipKOValidation, bool skipClientVersionRetrieval) + { + if (_data == null) + return new InitializationFailure("Failed to retrieve data from the target executeable."); + + if (!skipKOValidation && !ValidateKOClient()) + return new InitializationFailure("Failed to validate target as Knight Online executeable."); + + if (!skipClientVersionRetrieval && !InitClientVersion()) + return new InitializationFailure("Failed to retrieve target client version."); + + if (!InitKeysOffsets()) + return new InitializationFailure("Failed to retrieve all offsets for tbl keys."); + + return null; + } + + private bool ValidateKOClient() + { + // Validates that we load "Knight OnLine Client" PE. + var KnightOnLineClient = Pattern.Transform("4B 6E 69 67 68 74 20 4F 6E 4C 69 6E 65 20 43 6C 69 65 6E 74 00"); + long offset; + if (!Pattern.Find(_data, KnightOnLineClient, out offset)) + { + Console.WriteLine("[TargetPE::ValidateKOClient]: Doesn't seem like a valid KO exe."); + return false; + } + + return true; + } + + private bool InitClientVersion() + { + const int MAX_STEPS_FORWARD = 4; + const int MIN_CLIENT_VERSION = 1000; + const int MAX_CLIENT_VERSION = 5000; + + var pattern = Pattern.Transform("68 C3 12 00 00"); + long offset; + if (!Pattern.Find(_data, pattern, out offset)) + { + Console.WriteLine("[TargetPE::InitClientVersion]: Failed to find resource offset."); + return false; + } + + MemoryStream ms = (MemoryStream)PE.GetStream(); + ms.Seek(offset, SeekOrigin.Begin); + byte[] buff = new byte[4]; + ms.Read(buff, 0, 4, 1); + int resourceId = BitConverter.ToInt32(buff, 0); + if (resourceId != 4803) + { + Console.WriteLine($"[TargetPE::InitClientVersion]: Invalid resource id: {resourceId}." + + $"Are we at the right location? :)"); + return false; + } + + // https://c9x.me/x86/html/file_module_x86_id_35.html + byte[] cmps = { 0x81, 0x3D }; + + long originOffset = ms.Position; + long stepsBack = 72; + long startOffset = originOffset - stepsBack; + foreach (byte cmp in cmps) + { + ms.Position = startOffset; + + // while it's not a compare instruction, keep reading byte bites ;) + do + { + ms.Read(buff, 0, 1); + if (ms.Position > originOffset) + return false; + } while (buff[0] != cmp); + + for (int i = 0; i < MAX_STEPS_FORWARD; i++) + { + ms.Read(buff, 0, 4); + int version = BitConverter.ToInt32(buff, 0); + if (version > MIN_CLIENT_VERSION && version < MAX_CLIENT_VERSION) + { + ClientVersion = version; + Console.WriteLine("[TargetPE::InitClientVersion]: Found client version v" + ClientVersion); + return true; + } + ms.Position -= 3; + } + } + + return false; + } + + private bool InitKeysOffsets() + { + CryptoPatches.Clear(); + + // "N3TableBase - Can't open file(read) File Handle..." + var pattern = Pattern.Transform("4E 33 54 61 62 6C 65 42 61 73 65 20 2D 20 43"); // "N3TableBase - C" + long offsetLogWrite; + if (!Pattern.Find(_data, pattern, out offsetLogWrite)) + { + Console.WriteLine("[TargetPE::InitKeysOffsets]: Unable to find offset."); + return false; + } + + // Generate pattern for finding all references to that string + string logWriteVA = Calculator.OffsetToVA((ulong)offsetLogWrite).ToString("X8"); + string logWriteGenPattern = "68"; // push + for (int i = logWriteVA.Length - 1; i >= 0; --i) + logWriteGenPattern += i % 2 == 0 ? " " + logWriteVA.Substring(i, 2) : ""; + logWriteGenPattern += " E8"; // call + + Console.WriteLine($"[TargetPE::InitKeysOffsets]: Generated pattern result: {logWriteGenPattern}"); + var patternStrRefRegion = Pattern.Transform(logWriteGenPattern); + List regionStrRefOffsets; + if (!Pattern.FindAll(_data, patternStrRefRegion, out regionStrRefOffsets)) + { + Console.WriteLine("[TargetPE::InitKeysOffsets]: No offsets were found."); + return false; + } + + Console.WriteLine($"[TargetPE::InitKeysOffsets]: Found {regionStrRefOffsets.Count} region offsets."); + + var patDecryptRegions = new Pattern.Byte[][] { + // push ?? <- DWORD as short + // push ?? <- DWORD as short + // push ?? <- DWORD as short + Pattern.Transform("68 ?? ?? 00 00 68 ?? ?? 00 00 68 ?? ?? 00 00 ?? ?? E8"), + + //Pattern.Transform("FF ?? ?? ?? ?? ?? 8B ?? ?? ?? ?? ?? 55 FF"), + //Pattern.Transform("FF ?? ?? ?? ?? ?? 8B ?? ?? ?? ?? ?? 56 FF"), + //Pattern.Transform("FF ?? ?? ?? ?? ?? 8B ?? ?? ?? ?? ?? 57 FF"), + + // call ?? <- ReadFile + // push ebp|esi|edi + // call ?? <- CloseHandle + // xor reg,reg + Pattern.Transform("FF ?? ?? ?? ?? ?? 55 FF"), + Pattern.Transform("FF ?? ?? ?? ?? ?? 56 FF"), + Pattern.Transform("FF ?? ?? ?? ?? ?? 57 FF"), + }; + + const int ENCRYPT_REGION_MIN = 50; + const int ENCRYPT_REGION_MAX = 700; + const int ESTIMATED_DES_RANGE_BYTES = 280; + + bool detectedDesEncryption = false; + foreach (long regionStrRefOffset in regionStrRefOffsets) + { + long decryptRegionOffset = -1; + bool isInlinedFunction = false; + for (int i = 0; i < patDecryptRegions.Length; i++) + { + if (!Pattern.Find(_data, patDecryptRegions[i], out decryptRegionOffset, + regionStrRefOffset + ENCRYPT_REGION_MIN, regionStrRefOffset + ENCRYPT_REGION_MAX)) + continue; + + // Estimate average distance by bytes, to know whether there is more code for DES encryption before XOR. + if (!detectedDesEncryption && (decryptRegionOffset - regionStrRefOffset) > ESTIMATED_DES_RANGE_BYTES) + detectedDesEncryption = true; + + // First pattern: we know it's a function that got inlined by the compiler. + // +5 for ptr & +2 for xor reg,reg + isInlinedFunction = i != 0; + if (isInlinedFunction) + { + CanUpdateKey2 = false; + decryptRegionOffset += patDecryptRegions[i].Length + 5 + 2; + } + + break; + } + + if (decryptRegionOffset == -1) + { + Console.WriteLine($"[TargetPE::InitKeysOffsets]: Unable to find decryption region offset."); + continue; + } + + CryptoKey[] keys = new CryptoKey[CryptoPatch.KEYS_COUNT]; + + MemoryStream ms = (MemoryStream)PE.GetStream(); + ms.Seek(decryptRegionOffset, SeekOrigin.Begin); + + if (isInlinedFunction) + { + if (!InitInlinedKeys(keys, ms)) + return false; + } + else + { + for (int i = CryptoPatch.KEYS_COUNT - 1; i >= 0; i--) + { + DWORD dword; + ms.Position++; + dword = ms.ReadStructure(); + keys[i] = new CryptoKey((ms.Position - 4), (ushort)dword.sValue1, + (long)Calculator.OffsetToVA((ulong)(ms.Position - 4))); + } + } + long keyVA = (long)Calculator.OffsetToVA((ulong)decryptRegionOffset); + var patch = new CryptoPatch(decryptRegionOffset, keyVA, keys, isInlinedFunction); + CryptoPatches.Add(patch); + } + + // Update the container cryption type, so we know what we deal with + CryptoPatches.CryptoType = detectedDesEncryption ? CryptoType.DES_XOR : CryptoType.XOR; + + if (CryptoPatches.Count != regionStrRefOffsets.Count) + return false; + + if (!VerifyAllMatchedKeys(CryptoPatches)) + return false; + + return true; + } + + private bool InitInlinedKeys(CryptoKey[] keys, MemoryStream ms) + { + // Key1 find routine + var patKey1 = new Pattern.Byte[][] { + Pattern.Transform("B8 ?? ?? 00 00 7E"), + Pattern.Transform("BA ?? ?? 00 00 85"), + }; + + long key1Offset = -1; + for (int keyIndex = 0; keyIndex < patKey1.Length; keyIndex++) + { + if (Pattern.Find(_data, patKey1[keyIndex], out key1Offset, ms.Position, ms.Position + 500)) + { + ms.Position = key1Offset + 1; + break; + } + } + if (key1Offset == -1) + { + Console.WriteLine($"[TargetPE::InitInlinedKeys]: Unable to find key1."); + return false; + } + DWORD dword = ms.ReadStructure(); + keys[0] = new CryptoKey((ms.Position - 4), (ushort)dword.sValue1, + (long)Calculator.OffsetToVA((ulong)(ms.Position - 4))); + + // Key2 find routine + // TODO(Gilad): Key got inlined by multiple asm instructions...Will need to implement algorithm to + // calculate from instructions region of what's the key value (TryGettingInlinedKey2Value()). + keys[1] = new CryptoKey(-1, 0xFFFF, -1); + + // Key3 find routine + var patKey2 = new Pattern.Byte[][] { + Pattern.Transform("8D 94 02 ?? ?? 00 00 7C"), + Pattern.Transform("B8 ?? ?? 00 00 8D"), + Pattern.Transform("B8 ?? ?? 00 00 2B"), + }; + + long key2Offset = -1; + for (int keyIndex = 0; keyIndex < patKey2.Length; keyIndex++) + { + if (Pattern.Find(_data, patKey2[keyIndex], out key2Offset, ms.Position, ms.Position + 700)) + { + ms.Position = key2Offset + (keyIndex == 0 ? 3 : 1); + break; + } + } + if (key2Offset == -1) + { + Console.WriteLine($"[TargetPE::InitInlinedKeys]: Unable to find key2."); + return false; + } + dword = ms.ReadStructure(); + keys[2] = new CryptoKey((ms.Position - 4), (ushort)dword.sValue1, + (long)Calculator.OffsetToVA((ulong)(ms.Position - 4))); + + return true; + } + + private bool VerifyAllMatchedKeys(CryptoPatchContainer patches) + { + var samplePatch = CryptoPatches.Find(p => !p.Inlined); + var inlinedPatches = CryptoPatches.Where(p => p.Inlined); + foreach (var inlinedPatch in inlinedPatches) + { + if (inlinedPatch.Keys[0].Key != samplePatch.Keys[0].Key) + return false; + + if (inlinedPatch.Keys[2].Key != samplePatch.Keys[2].Key) + return false; + } + + var regularPatches = CryptoPatches.Where(p => !p.Inlined); + foreach (var patch in regularPatches) + { + for (int i = 0; i < CryptoPatch.KEYS_COUNT; i++) + { + if (patch.Keys[i].Key != samplePatch.Keys[i].Key) + return false; + } + } + + return true; + } + + public bool Patch(short key_r, short key_c1, short key_c2) + { + if (_data == null) + return false; + + short[] newKeys = { key_r, key_c1, key_c2 }; + using (var ms = new MemoryStream(_data)) + { + foreach (var patch in CryptoPatches) + { + for (int i = CryptoPatch.KEYS_COUNT - 1; i >= 0; i--) + { + if (i == 1 && !CanUpdateKey2) + continue; + + ms.Position = patch.Keys[i].Offset; + DWORD dword = (DWORD)newKeys[i]; + ms.WriteByte(dword.byValue1); + ms.WriteByte(dword.byValue2); + } + } + + if (FileInfo.Exists) + FileInfo.Delete(); + + File.WriteAllBytes(FileInfo.FullName, _data); + + // Re-initialize new changes, to verify we patched everything correctly. + if (!InitKeysOffsets()) + return false; + } + + return true; + } + + public bool PatchInlinedFunctions(short key_r, short key_c1, short key_c2) + { + if (_data == null) + return false; + + // TODO(Gilad): Implement. + + return true; + } + + private byte[] GetFileBytes(string filePath) + { + if (String.IsNullOrEmpty(filePath)) + throw new ArgumentNullException("[TargetPE::GetFileBytes]: File path cannot be null."); + if (!File.Exists(filePath)) + throw new FileNotFoundException("[TargetPE::GetFileBytes]: The path provided seem to be invalid."); + + if (Utility.IsFileLocked(filePath)) + return null; + + return File.ReadAllBytes(filePath); + } + + public bool CreateBackup(string destFilePath = "") + { + if (!IsValid) + return false; + + if (String.IsNullOrEmpty(destFilePath)) + destFilePath = FilePath; + + int iBackupCount = 1; + string dstFileTmp; + do + { + dstFileTmp = $"{destFilePath}_bak{iBackupCount++}"; + if (!File.Exists(dstFileTmp)) + { + FileInfo.CopyTo(dstFileTmp); + return true; + } + } while (File.Exists(dstFileTmp)); + + return false; + } + } +} diff --git a/source/KO-TBLCryptoEditor/KO-TBLCryptoEditor.csproj b/source/KO-TBLCryptoEditor/KO-TBLCryptoEditor.csproj new file mode 100644 index 0000000..3b10eb2 --- /dev/null +++ b/source/KO-TBLCryptoEditor/KO-TBLCryptoEditor.csproj @@ -0,0 +1,150 @@ + + + + + + Debug + AnyCPU + {29854838-9FCA-4090-BA6E-85CD54280847} + WinExe + KO.TBLCryptoEditor + KO-TBLCryptoEditor + v4.5 + 512 + true + true + + + + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + false + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + Resources/app.ico + + + + ..\..\packages\Costura.Fody.4.1.0\lib\net40\Costura.dll + + + ..\..\packages\WindowsAPICodePack-Core.1.1.1\lib\Microsoft.WindowsAPICodePack.dll + + + ..\..\packages\WindowsAPICodePack-Shell.1.1.1\lib\Microsoft.WindowsAPICodePack.Shell.dll + + + + + + + + + + + + + + + + Component + + + + + + + + + + + + Form + + + MainWindow.cs + + + + + + + + Form + + + ViewOffsetsWindow.cs + + + MainWindow.cs + + + ResXFileCodeGenerator + Resources.Designer.cs + Designer + + + True + Resources.resx + True + + + ViewOffsetsWindow.cs + + + + SettingsSingleFileGenerator + Settings.Designer.cs + + + True + Settings.settings + True + + + + + + + + + + + + + + {5ffb2393-5455-49c1-8d45-3ea5c0f22386} + PatternFinder + + + {a08f8a40-9eea-4a5d-b434-c4fbfd5effd8} + Workshell.PE + + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + \ No newline at end of file diff --git a/source/KO-TBLCryptoEditor/KO-TBLCryptoEditor.csproj.user b/source/KO-TBLCryptoEditor/KO-TBLCryptoEditor.csproj.user new file mode 100644 index 0000000..51f6223 --- /dev/null +++ b/source/KO-TBLCryptoEditor/KO-TBLCryptoEditor.csproj.user @@ -0,0 +1,13 @@ + + + + publish\ + + + + + + en-US + false + + \ No newline at end of file diff --git a/source/KO-TBLCryptoEditor/Program.cs b/source/KO-TBLCryptoEditor/Program.cs new file mode 100644 index 0000000..4eebe7d --- /dev/null +++ b/source/KO-TBLCryptoEditor/Program.cs @@ -0,0 +1,18 @@ +using System; +using System.Windows.Forms; + +using KO.TBLCryptoEditor.Views; + +namespace KO.TBLCryptoEditor +{ + static class Program + { + [STAThread] + static void Main() + { + Application.EnableVisualStyles(); + Application.SetCompatibleTextRenderingDefault(false); + Application.Run(new MainWindow()); + } + } +} diff --git a/source/KO-TBLCryptoEditor/Properties/AssemblyInfo.cs b/source/KO-TBLCryptoEditor/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..99c7303 --- /dev/null +++ b/source/KO-TBLCryptoEditor/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Knight Online Table Keys Encryption Editor")] +[assembly: AssemblyDescription("Changing Knight Online TBLs encryption made it easy.")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Knight Online Table Keys Encryption Editor")] +[assembly: AssemblyCopyright("Copyright © 2020 KO4Life")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("29854838-9fca-4090-ba6e-85cd54280847")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0")] +[assembly: AssemblyFileVersion("1.0.0")] diff --git a/source/KO-TBLCryptoEditor/Properties/Resources.Designer.cs b/source/KO-TBLCryptoEditor/Properties/Resources.Designer.cs new file mode 100644 index 0000000..073f0aa --- /dev/null +++ b/source/KO-TBLCryptoEditor/Properties/Resources.Designer.cs @@ -0,0 +1,83 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace KO.TBLCryptoEditor.Properties { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("KO.TBLCryptoEditor.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Icon similar to (Icon). + /// + internal static System.Drawing.Icon app { + get { + object obj = ResourceManager.GetObject("app", resourceCulture); + return ((System.Drawing.Icon)(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap ko4life { + get { + object obj = ResourceManager.GetObject("ko4life", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + } +} diff --git a/source/KO-TBLCryptoEditor/Properties/Resources.resx b/source/KO-TBLCryptoEditor/Properties/Resources.resx new file mode 100644 index 0000000..cd4b43f --- /dev/null +++ b/source/KO-TBLCryptoEditor/Properties/Resources.resx @@ -0,0 +1,127 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + + ..\resources\app.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + ..\Resources\ko4life.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + \ No newline at end of file diff --git a/source/KO-TBLCryptoEditor/Properties/Settings.Designer.cs b/source/KO-TBLCryptoEditor/Properties/Settings.Designer.cs new file mode 100644 index 0000000..8ced09a --- /dev/null +++ b/source/KO-TBLCryptoEditor/Properties/Settings.Designer.cs @@ -0,0 +1,26 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace KO.TBLCryptoEditor.Properties { + + + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "16.5.0.0")] + internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { + + private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); + + public static Settings Default { + get { + return defaultInstance; + } + } + } +} diff --git a/source/KO-TBLCryptoEditor/Properties/Settings.settings b/source/KO-TBLCryptoEditor/Properties/Settings.settings new file mode 100644 index 0000000..3964565 --- /dev/null +++ b/source/KO-TBLCryptoEditor/Properties/Settings.settings @@ -0,0 +1,7 @@ + + + + + + + diff --git a/source/KO-TBLCryptoEditor/Resources/app.ico b/source/KO-TBLCryptoEditor/Resources/app.ico new file mode 100644 index 0000000..52f95e8 Binary files /dev/null and b/source/KO-TBLCryptoEditor/Resources/app.ico differ diff --git a/source/KO-TBLCryptoEditor/Resources/ko4life.png b/source/KO-TBLCryptoEditor/Resources/ko4life.png new file mode 100644 index 0000000..e2630e2 Binary files /dev/null and b/source/KO-TBLCryptoEditor/Resources/ko4life.png differ diff --git a/source/KO-TBLCryptoEditor/Utils/DWORD.cs b/source/KO-TBLCryptoEditor/Utils/DWORD.cs new file mode 100644 index 0000000..d6c7f32 --- /dev/null +++ b/source/KO-TBLCryptoEditor/Utils/DWORD.cs @@ -0,0 +1,39 @@ +using System; +using System.Runtime.InteropServices; + +namespace KO.TBLCryptoEditor.Utils +{ + [StructLayout(LayoutKind.Explicit)] + public struct DWORD + { + [FieldOffset(0)] public int iValue; + [FieldOffset(0)] public float fValue; + [FieldOffset(0)] public short sValue1; + [FieldOffset(2)] public short sValue2; + [FieldOffset(0)] public byte byValue1; + [FieldOffset(1)] public byte byValue2; + [FieldOffset(2)] public byte byValue3; + [FieldOffset(3)] public byte byValue4; + + public static explicit operator DWORD(int value) + { + var dword = new DWORD(); + dword.iValue = value; + return dword; + } + + public static explicit operator DWORD(short value) + { + var dword = new DWORD(); + dword.sValue1 = value; + return dword; + } + + public static explicit operator DWORD(ushort value) + { + var dword = new DWORD(); + dword.sValue1 = (short)value; + return dword; + } + } +} diff --git a/source/KO-TBLCryptoEditor/Utils/FileUtil.cs b/source/KO-TBLCryptoEditor/Utils/FileUtil.cs new file mode 100644 index 0000000..ca4a612 --- /dev/null +++ b/source/KO-TBLCryptoEditor/Utils/FileUtil.cs @@ -0,0 +1,152 @@ +// Source: https://stackoverflow.com/a/20623311 +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.InteropServices; + +namespace KO.TBLCryptoEditor.Utils +{ + static public class FileUtil + { + [StructLayout(LayoutKind.Sequential)] + struct RM_UNIQUE_PROCESS + { + public int dwProcessId; + public System.Runtime.InteropServices.ComTypes.FILETIME ProcessStartTime; + } + + const int RmRebootReasonNone = 0; + const int CCH_RM_MAX_APP_NAME = 255; + const int CCH_RM_MAX_SVC_NAME = 63; + + enum RM_APP_TYPE + { + RmUnknownApp = 0, + RmMainWindow = 1, + RmOtherWindow = 2, + RmService = 3, + RmExplorer = 4, + RmConsole = 5, + RmCritical = 1000 + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + struct RM_PROCESS_INFO + { + public RM_UNIQUE_PROCESS Process; + + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = CCH_RM_MAX_APP_NAME + 1)] + public string strAppName; + + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = CCH_RM_MAX_SVC_NAME + 1)] + public string strServiceShortName; + + public RM_APP_TYPE ApplicationType; + public uint AppStatus; + public uint TSSessionId; + [MarshalAs(UnmanagedType.Bool)] + public bool bRestartable; + } + + [DllImport("rstrtmgr.dll", CharSet = CharSet.Unicode)] + static extern int RmRegisterResources(uint pSessionHandle, + UInt32 nFiles, + string[] rgsFilenames, + UInt32 nApplications, + [In] RM_UNIQUE_PROCESS[] rgApplications, + UInt32 nServices, + string[] rgsServiceNames); + + [DllImport("rstrtmgr.dll", CharSet = CharSet.Auto)] + static extern int RmStartSession(out uint pSessionHandle, int dwSessionFlags, string strSessionKey); + + [DllImport("rstrtmgr.dll")] + static extern int RmEndSession(uint pSessionHandle); + + [DllImport("rstrtmgr.dll")] + static extern int RmGetList(uint dwSessionHandle, + out uint pnProcInfoNeeded, + ref uint pnProcInfo, + [In, Out] RM_PROCESS_INFO[] rgAffectedApps, + ref uint lpdwRebootReasons); + + /// + /// Find out what process(es) have a lock on the specified file. + /// + /// Path of the file. + /// Processes locking the file + /// See also: + /// http://msdn.microsoft.com/en-us/library/windows/desktop/aa373661(v=vs.85).aspx + /// http://wyupdate.googlecode.com/svn-history/r401/trunk/frmFilesInUse.cs (no copyright in code at time of viewing) + /// + /// + static public List WhoIsLocking(string path) + { + uint handle; + string key = Guid.NewGuid().ToString(); + List processes = new List(); + + int res = RmStartSession(out handle, 0, key); + + if (res != 0) + throw new Exception("Could not begin restart session. Unable to determine file locker."); + + try + { + const int ERROR_MORE_DATA = 234; + uint pnProcInfoNeeded = 0, + pnProcInfo = 0, + lpdwRebootReasons = RmRebootReasonNone; + + string[] resources = new string[] { path }; // Just checking on one resource. + + res = RmRegisterResources(handle, (uint)resources.Length, resources, 0, null, 0, null); + + if (res != 0) + throw new Exception("Could not register resource."); + + //Note: there's a race condition here -- the first call to RmGetList() returns + // the total number of process. However, when we call RmGetList() again to get + // the actual processes this number may have increased. + res = RmGetList(handle, out pnProcInfoNeeded, ref pnProcInfo, null, ref lpdwRebootReasons); + + if (res == ERROR_MORE_DATA) + { + // Create an array to store the process results + RM_PROCESS_INFO[] processInfo = new RM_PROCESS_INFO[pnProcInfoNeeded]; + pnProcInfo = pnProcInfoNeeded; + + // Get the list + res = RmGetList(handle, out pnProcInfoNeeded, ref pnProcInfo, processInfo, ref lpdwRebootReasons); + + if (res == 0) + { + processes = new List((int)pnProcInfo); + + // Enumerate all of the results and add them to the + // list to be returned + for (int i = 0; i < pnProcInfo; i++) + { + try + { + processes.Add(Process.GetProcessById(processInfo[i].Process.dwProcessId)); + } + // catch the error -- in case the process is no longer running + catch (ArgumentException) { } + } + } + else + throw new Exception("Could not list processes locking resource."); + } + else if (res != 0) + throw new Exception("Could not list processes locking resource. Failed to get size of result."); + } + finally + { + RmEndSession(handle); + } + + return processes; + } + } +} diff --git a/source/KO-TBLCryptoEditor/Utils/HtmlTable.cs b/source/KO-TBLCryptoEditor/Utils/HtmlTable.cs new file mode 100644 index 0000000..0524c63 --- /dev/null +++ b/source/KO-TBLCryptoEditor/Utils/HtmlTable.cs @@ -0,0 +1,83 @@ +using System; +using System.Text; + +namespace KO.TBLCryptoEditor.Utils +{ + public class HtmlTable : IDisposable + { + private StringBuilder _data; + private string _id; + private string _class; + private string _caption; + + public string Caption + { + get => _caption; + set => SetCaption(value); + } + + public string Id + { + get => _id; + set => SetId(value); + } + + public string @Class + { + get => _class; + set => SetClass(value); + } + + + public HtmlTable(StringBuilder sb, string caption = "", string id = "", string @class = "") + { + _data = sb; + _caption = caption; + _id = id; + _class = @class; + + _data.Append($"\n"); + _data.Append($""); + } + + public HtmlTableRow AddRow() + { + return new HtmlTableRow(_data); + } + + public HtmlTableRow AddHeaderRow() + { + return new HtmlTableRow(_data, true); + } + + public void StartTableBody() + { + _data.Append(""); + } + + public void EndTableBody() + { + _data.Append(""); + } + + private void SetCaption(string caption) + { + _data.Replace($"", $""); + } + + private void SetId(string id) + { + _data.Replace($"
{caption}{_caption}{caption}
", $"
"); + } + + private void SetClass(string @class) + { + _data.Replace($"
", $"
"); + } + + public void Dispose() + { + _data.Append("
"); + } + } +} diff --git a/source/KO-TBLCryptoEditor/Utils/HtmlTableRow.cs b/source/KO-TBLCryptoEditor/Utils/HtmlTableRow.cs new file mode 100644 index 0000000..011d4d9 --- /dev/null +++ b/source/KO-TBLCryptoEditor/Utils/HtmlTableRow.cs @@ -0,0 +1,47 @@ +using System; +using System.Text; + +namespace KO.TBLCryptoEditor.Utils +{ + public class HtmlTableRow : IDisposable + { + private StringBuilder _data; + private bool _isHeader; + + public HtmlTableRow(StringBuilder sb, bool isHeader = false) + { + _data = sb; + _isHeader = isHeader; + if (_isHeader) + { + _data.Append("\n"); + } + _data.Append("\t\n"); + } + + public void AddCell(string innerText) + { + if (_isHeader) + { + _data.Append("\t\t\n"); + _data.Append("\t\t\t" + innerText); + _data.Append("\t\t\n"); + } + else + { + _data.Append("\t\t\n"); + _data.Append("\t\t\t" + innerText); + _data.Append("\t\t\n"); + } + } + + public void Dispose() + { + _data.Append("\t\n"); + if (_isHeader) + { + _data.Append("\n"); + } + } + } +} diff --git a/source/KO-TBLCryptoEditor/Utils/LocationCalculator.cs b/source/KO-TBLCryptoEditor/Utils/LocationCalculator.cs new file mode 100644 index 0000000..f5c31d4 --- /dev/null +++ b/source/KO-TBLCryptoEditor/Utils/LocationCalculator.cs @@ -0,0 +1,220 @@ +#region License +// Copyright(c) Workshell Ltd +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +#endregion + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Workshell.PE +{ + public sealed class LocationCalculator + { + private PortableExecutableImage _image; + + internal LocationCalculator(PortableExecutableImage image) + { + _image = image; + } + + #region Methods + + public Location OffsetToLocation(ulong offset, ulong size) + { + var rva = OffsetToRVA(offset); + var va = OffsetToVA(offset); + + return new Location(_image, offset, rva, va, size, size); + } + + /* VA */ + + public Section VAToSection(ulong va) + { + var imageBase = _image.NTHeaders.OptionalHeader.ImageBase; + var rva = Convert.ToUInt32(va - imageBase); + + return RVAToSection(rva); + } + + public SectionTableEntry VAToSectionTableEntry(ulong va) + { + var imageBase = _image.NTHeaders.OptionalHeader.ImageBase; + var rva = Convert.ToUInt32(va - imageBase); + + return RVAToSectionTableEntry(rva); + } + + public ulong VAToOffset(ulong va) + { + var imageBase = _image.NTHeaders.OptionalHeader.ImageBase; + var rva = Convert.ToUInt32(va - imageBase); + + return RVAToOffset(rva); + } + + public ulong VAToOffset(Section section, ulong va) + { + return VAToOffset(section.TableEntry,va); + } + + public ulong VAToOffset(SectionTableEntry section, ulong va) + { + var imageBase = _image.NTHeaders.OptionalHeader.ImageBase; + var rva = Convert.ToUInt32(va - imageBase); + + return RVAToOffset(section,rva); + } + + public ulong OffsetToVA(ulong offset) + { + var entries = _image.SectionTable.OrderBy(e => e.PointerToRawData).ToArray(); + SectionTableEntry entry = null; + + for(var i = 0; i < entries.Length; i++) + { + if (offset >= entries[i].PointerToRawData && offset < (entries[i].PointerToRawData + entries[i].SizeOfRawData)) + entry = entries[i]; + } + + if (entry != null) + return OffsetToVA(entry, offset); + + return 0; + } + + public ulong OffsetToVA(Section section, ulong offset) + { + return OffsetToVA(section.TableEntry,offset); + } + + public ulong OffsetToVA(SectionTableEntry section, ulong offset) + { + var imageBase = _image.NTHeaders.OptionalHeader.ImageBase; + var rva = Convert.ToUInt32((offset + section.VirtualAddress) - section.PointerToRawData); + + return imageBase + rva; + } + + /* RVA */ + + public Section RVAToSection(uint rva) + { + var entry = RVAToSectionTableEntry(rva); + + if (entry == null) + { + return null; + } + + return _image.Sections[entry]; + } + + public SectionTableEntry RVAToSectionTableEntry(uint rva) + { + if (_image.SectionTable == null) + { + return null; + } + + var entries = _image.SectionTable.OrderBy(e => e.VirtualAddress).ToArray(); + SectionTableEntry entry = null; + + for (var i = 0; i < entries.Length; i++) + { + var maxRVA = entries[i].VirtualAddress + entries[i].SizeOfRawData; + + if (i != (entries.Length - 1)) + { + maxRVA = entries[i + 1].VirtualAddress; + } + + if (rva >= entries[i].VirtualAddress && rva < maxRVA) + { + entry = entries[i]; + } + } + + return entry; + } + + public ulong RVAToOffset(uint rva) + { + var entries = _image.SectionTable.OrderBy(e => e.VirtualAddress).ToArray(); + SectionTableEntry entry = null; + + for (var i = 0; i < entries.Length; i++) + { + if (rva >= entries[i].VirtualAddress && rva < (entries[i].VirtualAddress + entries[i].SizeOfRawData)) + entry = entries[i]; + } + + if (entry != null) + return RVAToOffset(entry, rva); + + return 0; + } + + public ulong RVAToOffset(Section section, uint rva) + { + return RVAToOffset(section.TableEntry,rva); + } + + public ulong RVAToOffset(SectionTableEntry section, uint rva) + { + var offset = (rva - section.VirtualAddress) + section.PointerToRawData; + + return offset; + } + + public uint OffsetToRVA(ulong offset) + { + var entries = _image.SectionTable.OrderBy(e => e.PointerToRawData).ToArray(); + SectionTableEntry entry = null; + + for (var i = 0; i < entries.Length; i++) + { + if (offset >= entries[i].PointerToRawData && offset < (entries[i].PointerToRawData + entries[i].SizeOfRawData)) + entry = entries[i]; + } + + if (entry != null) + return OffsetToRVA(entry, offset); + + return 0; + } + + public uint OffsetToRVA(Section section, ulong offset) + { + return OffsetToRVA(section.TableEntry,offset); + } + + public uint OffsetToRVA(SectionTableEntry section, ulong offset) + { + var rva = Convert.ToUInt32((offset + section.VirtualAddress) - section.PointerToRawData); + + return rva; + } + + #endregion + } +} diff --git a/source/KO-TBLCryptoEditor/Utils/PEHeaderReader.cs b/source/KO-TBLCryptoEditor/Utils/PEHeaderReader.cs new file mode 100644 index 0000000..cddd482 --- /dev/null +++ b/source/KO-TBLCryptoEditor/Utils/PEHeaderReader.cs @@ -0,0 +1,566 @@ +// Source: https://web.archive.org/web/20160312145447/http://code.cheesydesign.com/?p=572 +using System; +using System.Runtime.InteropServices; +using System.IO; + +namespace KO.TBLCryptoEditor.Utils +{ + + /// + /// Reads in the header information of the Portable Executable format. + /// Provides information such as the date the assembly was compiled. + /// + public class PEHeaderReader + { + #region File Header Structures + + public struct IMAGE_DOS_HEADER + { // DOS .EXE header + public UInt16 e_magic; // Magic number + public UInt16 e_cblp; // Bytes on last page of file + public UInt16 e_cp; // Pages in file + public UInt16 e_crlc; // Relocations + public UInt16 e_cparhdr; // Size of header in paragraphs + public UInt16 e_minalloc; // Minimum extra paragraphs needed + public UInt16 e_maxalloc; // Maximum extra paragraphs needed + public UInt16 e_ss; // Initial (relative) SS value + public UInt16 e_sp; // Initial SP value + public UInt16 e_csum; // Checksum + public UInt16 e_ip; // Initial IP value + public UInt16 e_cs; // Initial (relative) CS value + public UInt16 e_lfarlc; // File address of relocation table + public UInt16 e_ovno; // Overlay number + public UInt16 e_res_0; // Reserved words + public UInt16 e_res_1; // Reserved words + public UInt16 e_res_2; // Reserved words + public UInt16 e_res_3; // Reserved words + public UInt16 e_oemid; // OEM identifier (for e_oeminfo) + public UInt16 e_oeminfo; // OEM information; e_oemid specific + public UInt16 e_res2_0; // Reserved words + public UInt16 e_res2_1; // Reserved words + public UInt16 e_res2_2; // Reserved words + public UInt16 e_res2_3; // Reserved words + public UInt16 e_res2_4; // Reserved words + public UInt16 e_res2_5; // Reserved words + public UInt16 e_res2_6; // Reserved words + public UInt16 e_res2_7; // Reserved words + public UInt16 e_res2_8; // Reserved words + public UInt16 e_res2_9; // Reserved words + public UInt32 e_lfanew; // File address of new exe header + } + + [StructLayout(LayoutKind.Sequential)] + public struct IMAGE_DATA_DIRECTORY + { + public UInt32 VirtualAddress; + public UInt32 Size; + } + + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct IMAGE_OPTIONAL_HEADER32 + { + public UInt16 Magic; + public Byte MajorLinkerVersion; + public Byte MinorLinkerVersion; + public UInt32 SizeOfCode; + public UInt32 SizeOfInitializedData; + public UInt32 SizeOfUninitializedData; + public UInt32 AddressOfEntryPoint; + public UInt32 BaseOfCode; + public UInt32 BaseOfData; + public UInt32 ImageBase; + public UInt32 SectionAlignment; + public UInt32 FileAlignment; + public UInt16 MajorOperatingSystemVersion; + public UInt16 MinorOperatingSystemVersion; + public UInt16 MajorImageVersion; + public UInt16 MinorImageVersion; + public UInt16 MajorSubsystemVersion; + public UInt16 MinorSubsystemVersion; + public UInt32 Win32VersionValue; + public UInt32 SizeOfImage; + public UInt32 SizeOfHeaders; + public UInt32 CheckSum; + public UInt16 Subsystem; + public UInt16 DllCharacteristics; + public UInt32 SizeOfStackReserve; + public UInt32 SizeOfStackCommit; + public UInt32 SizeOfHeapReserve; + public UInt32 SizeOfHeapCommit; + public UInt32 LoaderFlags; + public UInt32 NumberOfRvaAndSizes; + + public IMAGE_DATA_DIRECTORY ExportTable; + public IMAGE_DATA_DIRECTORY ImportTable; + public IMAGE_DATA_DIRECTORY ResourceTable; + public IMAGE_DATA_DIRECTORY ExceptionTable; + public IMAGE_DATA_DIRECTORY CertificateTable; + public IMAGE_DATA_DIRECTORY BaseRelocationTable; + public IMAGE_DATA_DIRECTORY Debug; + public IMAGE_DATA_DIRECTORY Architecture; + public IMAGE_DATA_DIRECTORY GlobalPtr; + public IMAGE_DATA_DIRECTORY TLSTable; + public IMAGE_DATA_DIRECTORY LoadConfigTable; + public IMAGE_DATA_DIRECTORY BoundImport; + public IMAGE_DATA_DIRECTORY IAT; + public IMAGE_DATA_DIRECTORY DelayImportDescriptor; + public IMAGE_DATA_DIRECTORY CLRRuntimeHeader; + public IMAGE_DATA_DIRECTORY Reserved; + } + + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct IMAGE_OPTIONAL_HEADER64 + { + public UInt16 Magic; + public Byte MajorLinkerVersion; + public Byte MinorLinkerVersion; + public UInt32 SizeOfCode; + public UInt32 SizeOfInitializedData; + public UInt32 SizeOfUninitializedData; + public UInt32 AddressOfEntryPoint; + public UInt32 BaseOfCode; + public UInt64 ImageBase; + public UInt32 SectionAlignment; + public UInt32 FileAlignment; + public UInt16 MajorOperatingSystemVersion; + public UInt16 MinorOperatingSystemVersion; + public UInt16 MajorImageVersion; + public UInt16 MinorImageVersion; + public UInt16 MajorSubsystemVersion; + public UInt16 MinorSubsystemVersion; + public UInt32 Win32VersionValue; + public UInt32 SizeOfImage; + public UInt32 SizeOfHeaders; + public UInt32 CheckSum; + public UInt16 Subsystem; + public UInt16 DllCharacteristics; + public UInt64 SizeOfStackReserve; + public UInt64 SizeOfStackCommit; + public UInt64 SizeOfHeapReserve; + public UInt64 SizeOfHeapCommit; + public UInt32 LoaderFlags; + public UInt32 NumberOfRvaAndSizes; + + public IMAGE_DATA_DIRECTORY ExportTable; + public IMAGE_DATA_DIRECTORY ImportTable; + public IMAGE_DATA_DIRECTORY ResourceTable; + public IMAGE_DATA_DIRECTORY ExceptionTable; + public IMAGE_DATA_DIRECTORY CertificateTable; + public IMAGE_DATA_DIRECTORY BaseRelocationTable; + public IMAGE_DATA_DIRECTORY Debug; + public IMAGE_DATA_DIRECTORY Architecture; + public IMAGE_DATA_DIRECTORY GlobalPtr; + public IMAGE_DATA_DIRECTORY TLSTable; + public IMAGE_DATA_DIRECTORY LoadConfigTable; + public IMAGE_DATA_DIRECTORY BoundImport; + public IMAGE_DATA_DIRECTORY IAT; + public IMAGE_DATA_DIRECTORY DelayImportDescriptor; + public IMAGE_DATA_DIRECTORY CLRRuntimeHeader; + public IMAGE_DATA_DIRECTORY Reserved; + } + + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct IMAGE_FILE_HEADER + { + public UInt16 Machine; + public UInt16 NumberOfSections; + public UInt32 TimeDateStamp; + public UInt32 PointerToSymbolTable; + public UInt32 NumberOfSymbols; + public UInt16 SizeOfOptionalHeader; + public UInt16 Characteristics; + } + + // Grabbed the following 2 definitions from http://www.pinvoke.net/default.aspx/Structures/IMAGE_SECTION_HEADER.html + + [StructLayout(LayoutKind.Explicit)] + public struct IMAGE_SECTION_HEADER + { + [FieldOffset(0)] + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)] + public char[] Name; + [FieldOffset(8)] + public UInt32 VirtualSize; + [FieldOffset(12)] + public UInt32 VirtualAddress; + [FieldOffset(16)] + public UInt32 SizeOfRawData; + [FieldOffset(20)] + public UInt32 PointerToRawData; + [FieldOffset(24)] + public UInt32 PointerToRelocations; + [FieldOffset(28)] + public UInt32 PointerToLinenumbers; + [FieldOffset(32)] + public UInt16 NumberOfRelocations; + [FieldOffset(34)] + public UInt16 NumberOfLinenumbers; + [FieldOffset(36)] + public DataSectionFlags Characteristics; + + public string Section + { + get { return new string(Name); } + } + } + + [Flags] + public enum DataSectionFlags : uint + { + /// + /// Reserved for future use. + /// + TypeReg = 0x00000000, + /// + /// Reserved for future use. + /// + TypeDsect = 0x00000001, + /// + /// Reserved for future use. + /// + TypeNoLoad = 0x00000002, + /// + /// Reserved for future use. + /// + TypeGroup = 0x00000004, + /// + /// The section should not be padded to the next boundary. This flag is obsolete and is replaced by IMAGE_SCN_ALIGN_1BYTES. This is valid only for object files. + /// + TypeNoPadded = 0x00000008, + /// + /// Reserved for future use. + /// + TypeCopy = 0x00000010, + /// + /// The section contains executable code. + /// + ContentCode = 0x00000020, + /// + /// The section contains initialized data. + /// + ContentInitializedData = 0x00000040, + /// + /// The section contains uninitialized data. + /// + ContentUninitializedData = 0x00000080, + /// + /// Reserved for future use. + /// + LinkOther = 0x00000100, + /// + /// The section contains comments or other information. The .drectve section has this type. This is valid for object files only. + /// + LinkInfo = 0x00000200, + /// + /// Reserved for future use. + /// + TypeOver = 0x00000400, + /// + /// The section will not become part of the image. This is valid only for object files. + /// + LinkRemove = 0x00000800, + /// + /// The section contains COMDAT data. For more information, see section 5.5.6, COMDAT Sections (Object Only). This is valid only for object files. + /// + LinkComDat = 0x00001000, + /// + /// Reset speculative exceptions handling bits in the TLB entries for this section. + /// + NoDeferSpecExceptions = 0x00004000, + /// + /// The section contains data referenced through the global pointer (GP). + /// + RelativeGP = 0x00008000, + /// + /// Reserved for future use. + /// + MemPurgeable = 0x00020000, + /// + /// Reserved for future use. + /// + Memory16Bit = 0x00020000, + /// + /// Reserved for future use. + /// + MemoryLocked = 0x00040000, + /// + /// Reserved for future use. + /// + MemoryPreload = 0x00080000, + /// + /// Align data on a 1-byte boundary. Valid only for object files. + /// + Align1Bytes = 0x00100000, + /// + /// Align data on a 2-byte boundary. Valid only for object files. + /// + Align2Bytes = 0x00200000, + /// + /// Align data on a 4-byte boundary. Valid only for object files. + /// + Align4Bytes = 0x00300000, + /// + /// Align data on an 8-byte boundary. Valid only for object files. + /// + Align8Bytes = 0x00400000, + /// + /// Align data on a 16-byte boundary. Valid only for object files. + /// + Align16Bytes = 0x00500000, + /// + /// Align data on a 32-byte boundary. Valid only for object files. + /// + Align32Bytes = 0x00600000, + /// + /// Align data on a 64-byte boundary. Valid only for object files. + /// + Align64Bytes = 0x00700000, + /// + /// Align data on a 128-byte boundary. Valid only for object files. + /// + Align128Bytes = 0x00800000, + /// + /// Align data on a 256-byte boundary. Valid only for object files. + /// + Align256Bytes = 0x00900000, + /// + /// Align data on a 512-byte boundary. Valid only for object files. + /// + Align512Bytes = 0x00A00000, + /// + /// Align data on a 1024-byte boundary. Valid only for object files. + /// + Align1024Bytes = 0x00B00000, + /// + /// Align data on a 2048-byte boundary. Valid only for object files. + /// + Align2048Bytes = 0x00C00000, + /// + /// Align data on a 4096-byte boundary. Valid only for object files. + /// + Align4096Bytes = 0x00D00000, + /// + /// Align data on an 8192-byte boundary. Valid only for object files. + /// + Align8192Bytes = 0x00E00000, + /// + /// The section contains extended relocations. + /// + LinkExtendedRelocationOverflow = 0x01000000, + /// + /// The section can be discarded as needed. + /// + MemoryDiscardable = 0x02000000, + /// + /// The section cannot be cached. + /// + MemoryNotCached = 0x04000000, + /// + /// The section is not pageable. + /// + MemoryNotPaged = 0x08000000, + /// + /// The section can be shared in memory. + /// + MemoryShared = 0x10000000, + /// + /// The section can be executed as code. + /// + MemoryExecute = 0x20000000, + /// + /// The section can be read. + /// + MemoryRead = 0x40000000, + /// + /// The section can be written to. + /// + MemoryWrite = 0x80000000 + } + + #endregion File Header Structures + + #region Private Fields + + /// + /// The DOS header + /// + private IMAGE_DOS_HEADER dosHeader; + /// + /// The file header + /// + private IMAGE_FILE_HEADER fileHeader; + /// + /// Optional 32 bit file header + /// + private IMAGE_OPTIONAL_HEADER32 optionalHeader32; + /// + /// Optional 64 bit file header + /// + private IMAGE_OPTIONAL_HEADER64 optionalHeader64; + /// + /// Image Section headers. Number of sections is in the file header. + /// + private IMAGE_SECTION_HEADER[] imageSectionHeaders; + + #endregion Private Fields + + #region Public Methods + + public PEHeaderReader(string filePath) + { + // Read in the DLL or EXE and get the timestamp + using (FileStream stream = new FileStream(filePath, System.IO.FileMode.Open, System.IO.FileAccess.Read)) + { + BinaryReader reader = new BinaryReader(stream); + dosHeader = FromBinaryReader(reader); + + // Add 4 bytes to the offset + stream.Seek(dosHeader.e_lfanew, SeekOrigin.Begin); + + UInt32 ntHeadersSignature = reader.ReadUInt32(); + fileHeader = FromBinaryReader(reader); + if (this.Is32BitHeader) + { + optionalHeader32 = FromBinaryReader(reader); + } + else + { + optionalHeader64 = FromBinaryReader(reader); + } + + imageSectionHeaders = new IMAGE_SECTION_HEADER[fileHeader.NumberOfSections]; + for (int headerNo = 0; headerNo < imageSectionHeaders.Length; ++headerNo) + { + imageSectionHeaders[headerNo] = FromBinaryReader(reader); + } + + } + } + + /// + /// Gets the header of the .NET assembly that called this function + /// + /// + public static PEHeaderReader GetCallingAssemblyHeader() + { + // Get the path to the calling assembly, which is the path to the + // DLL or EXE that we want the time of + string filePath = System.Reflection.Assembly.GetCallingAssembly().Location; + + // Get and return the timestamp + return new PEHeaderReader(filePath); + } + + /// + /// Gets the header of the .NET assembly that called this function + /// + /// + public static PEHeaderReader GetAssemblyHeader() + { + // Get the path to the calling assembly, which is the path to the + // DLL or EXE that we want the time of + string filePath = System.Reflection.Assembly.GetAssembly(typeof(PEHeaderReader)).Location; + + // Get and return the timestamp + return new PEHeaderReader(filePath); + } + + /// + /// Reads in a block from a file and converts it to the struct + /// type specified by the template parameter + /// + /// + /// + /// + public static T FromBinaryReader(BinaryReader reader) + { + // Read in a byte array + byte[] bytes = reader.ReadBytes(Marshal.SizeOf(typeof(T))); + + // Pin the managed memory while, copy it out the data, then unpin it + GCHandle handle = GCHandle.Alloc(bytes, GCHandleType.Pinned); + T theStructure = (T)Marshal.PtrToStructure(handle.AddrOfPinnedObject(), typeof(T)); + handle.Free(); + + return theStructure; + } + + #endregion Public Methods + + #region Properties + + /// + /// Gets if the file header is 32 bit or not + /// + public bool Is32BitHeader + { + get + { + UInt16 IMAGE_FILE_32BIT_MACHINE = 0x0100; + return (IMAGE_FILE_32BIT_MACHINE & FileHeader.Characteristics) == IMAGE_FILE_32BIT_MACHINE; + } + } + + /// + /// Gets the file header + /// + public IMAGE_FILE_HEADER FileHeader + { + get + { + return fileHeader; + } + } + + /// + /// Gets the optional header + /// + public IMAGE_OPTIONAL_HEADER32 OptionalHeader32 + { + get + { + return optionalHeader32; + } + } + + /// + /// Gets the optional header + /// + public IMAGE_OPTIONAL_HEADER64 OptionalHeader64 + { + get + { + return optionalHeader64; + } + } + + public IMAGE_SECTION_HEADER[] ImageSectionHeaders + { + get + { + return imageSectionHeaders; + } + } + + /// + /// Gets the timestamp from the file header + /// + public DateTime TimeStamp + { + get + { + // Timestamp is a date offset from 1970 + DateTime returnValue = new DateTime(1970, 1, 1, 0, 0, 0); + + // Add in the number of seconds since 1970/1/1 + returnValue = returnValue.AddSeconds(fileHeader.TimeDateStamp); + // Adjust to local timezone + returnValue += TimeZone.CurrentTimeZone.GetUtcOffset(returnValue); + + return returnValue; + } + } + + #endregion Properties + } +} diff --git a/source/KO-TBLCryptoEditor/Utils/Utility.cs b/source/KO-TBLCryptoEditor/Utils/Utility.cs new file mode 100644 index 0000000..feea82b --- /dev/null +++ b/source/KO-TBLCryptoEditor/Utils/Utility.cs @@ -0,0 +1,79 @@ +using System; +using System.IO; +using System.Linq; +using System.Diagnostics; +using System.Collections.Generic; +using System.Runtime.InteropServices; + +namespace KO.TBLCryptoEditor.Utils +{ + public static class Utility + { + public static string TimeStamp => DateTime.Now.ToString("T"); + + public static void ForEach(this IEnumerable source, Action action) + { + foreach (T element in source) + action(element); + } + + public static void MemSet(this byte[] source, byte value) + { + for (int i = 0; i < source.Length; i++) + { + source[i] = value; + } + } + + public static int Read(this MemoryStream source, byte[] buffer, int offset, int count, int fromPosition) + { + source.Position += fromPosition; + return source.Read(buffer, offset, count); + } + + public static T ReadStructure(this Stream stream) where T : struct + { + if (stream == null) + throw new ArgumentNullException(); + + int size = Marshal.SizeOf(typeof(T)); + byte[] bytes = new byte[size]; + if (stream.Read(bytes, 0, size) != size) + throw new EndOfStreamException(); + + GCHandle handle = GCHandle.Alloc(bytes, GCHandleType.Pinned); + try { + return (T)Marshal.PtrToStructure(handle.AddrOfPinnedObject(), typeof(T)); + } + finally { + handle.Free(); + } + } + + public static void CopyFilesRecursively(this DirectoryInfo src, DirectoryInfo dest) + { + foreach (DirectoryInfo di in src.GetDirectories()) + CopyFilesRecursively(di, dest.CreateSubdirectory(di.Name)); + + foreach (FileInfo fi in src.GetFiles()) + fi.CopyTo(Path.Combine(dest.FullName, fi.Name)); + } + + public static bool IsFileLocked(string filePath) + { + List lockers = FileUtil.WhoIsLocking(filePath); + return lockers.Any(); + } + + public static string WhoIsFileLocking(string filePath) + { + List lockers = FileUtil.WhoIsLocking(filePath); + if (!lockers.Any()) + return String.Empty; + + string procs = String.Empty; + lockers.ForEach(p => procs += $"{p.ProcessName}.exe, "); + return procs.Substring(0, procs.Length - 2); + } + } +} diff --git a/source/KO-TBLCryptoEditor/Views/MainWindow.Designer.cs b/source/KO-TBLCryptoEditor/Views/MainWindow.Designer.cs new file mode 100644 index 0000000..70cb2d6 --- /dev/null +++ b/source/KO-TBLCryptoEditor/Views/MainWindow.Designer.cs @@ -0,0 +1,326 @@ +namespace KO.TBLCryptoEditor.Views +{ + partial class MainWindow + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.components = new System.ComponentModel.Container(); + this.btnGenerateKeys = new System.Windows.Forms.Button(); + this.btnUpdateClient = new System.Windows.Forms.Button(); + this.statusStrip = new System.Windows.Forms.StatusStrip(); + this.lblStatus = new System.Windows.Forms.ToolStripStatusLabel(); + this.cbxManualUpdate = new System.Windows.Forms.CheckBox(); + this.gbxKeys = new System.Windows.Forms.GroupBox(); + this.label3 = new System.Windows.Forms.Label(); + this.label2 = new System.Windows.Forms.Label(); + this.label1 = new System.Windows.Forms.Label(); + this.tbxKey1 = new KO.TBLCryptoEditor.Controls.CryptoKeyInput(); + this.tbxKey2 = new KO.TBLCryptoEditor.Controls.CryptoKeyInput(); + this.tbxKey3 = new KO.TBLCryptoEditor.Controls.CryptoKeyInput(); + this.cbxCreateBackup = new System.Windows.Forms.CheckBox(); + this.gbxOptions = new System.Windows.Forms.GroupBox(); + this.cbxSkipClientVersion = new System.Windows.Forms.CheckBox(); + this.cbxSkipKOValidation = new System.Windows.Forms.CheckBox(); + this.toolTip = new System.Windows.Forms.ToolTip(this.components); + this.btnViewOffsets = new System.Windows.Forms.Button(); + this.btnUpdateData = new System.Windows.Forms.Button(); + this.panelDragArea = new System.Windows.Forms.Panel(); + this.statusStrip.SuspendLayout(); + this.gbxKeys.SuspendLayout(); + this.gbxOptions.SuspendLayout(); + this.SuspendLayout(); + // + // btnGenerateKeys + // + this.btnGenerateKeys.Enabled = false; + this.btnGenerateKeys.Location = new System.Drawing.Point(241, 143); + this.btnGenerateKeys.Name = "btnGenerateKeys"; + this.btnGenerateKeys.Size = new System.Drawing.Size(153, 32); + this.btnGenerateKeys.TabIndex = 0; + this.btnGenerateKeys.Text = "Generate New Random Keys"; + this.toolTip.SetToolTip(this.btnGenerateKeys, "Randomly generate new keys."); + this.btnGenerateKeys.UseVisualStyleBackColor = true; + this.btnGenerateKeys.Click += new System.EventHandler(this.btnGenerateKeys_Click); + // + // btnUpdateClient + // + this.btnUpdateClient.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right))); + this.btnUpdateClient.Enabled = false; + this.btnUpdateClient.Location = new System.Drawing.Point(402, 181); + this.btnUpdateClient.Name = "btnUpdateClient"; + this.btnUpdateClient.Size = new System.Drawing.Size(153, 32); + this.btnUpdateClient.TabIndex = 2; + this.btnUpdateClient.Text = "Update Client Encryption"; + this.toolTip.SetToolTip(this.btnUpdateClient, "New keys will be applied inside the executeable."); + this.btnUpdateClient.UseVisualStyleBackColor = true; + this.btnUpdateClient.Click += new System.EventHandler(this.btnPatchClient_Click); + // + // statusStrip + // + this.statusStrip.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { + this.lblStatus}); + this.statusStrip.Location = new System.Drawing.Point(0, 223); + this.statusStrip.Name = "statusStrip"; + this.statusStrip.Size = new System.Drawing.Size(566, 22); + this.statusStrip.SizingGrip = false; + this.statusStrip.TabIndex = 6; + this.statusStrip.Text = "statusStrip1"; + // + // lblStatus + // + this.lblStatus.Name = "lblStatus"; + this.lblStatus.Size = new System.Drawing.Size(130, 17); + this.lblStatus.Text = "Waiting for user action."; + // + // cbxManualUpdate + // + this.cbxManualUpdate.AutoSize = true; + this.cbxManualUpdate.Enabled = false; + this.cbxManualUpdate.Location = new System.Drawing.Point(12, 102); + this.cbxManualUpdate.Name = "cbxManualUpdate"; + this.cbxManualUpdate.Size = new System.Drawing.Size(101, 17); + this.cbxManualUpdate.TabIndex = 3; + this.cbxManualUpdate.Text = "Manual Change"; + this.toolTip.SetToolTip(this.cbxManualUpdate, "Manually update the tbl keys. If you can\'t update Key2, this is because there wer" + + "e inlined functions in that exe."); + this.cbxManualUpdate.UseVisualStyleBackColor = true; + this.cbxManualUpdate.CheckedChanged += new System.EventHandler(this.cbxManualUpdate_CheckedChanged); + // + // gbxKeys + // + this.gbxKeys.Controls.Add(this.label3); + this.gbxKeys.Controls.Add(this.label2); + this.gbxKeys.Controls.Add(this.label1); + this.gbxKeys.Controls.Add(this.tbxKey1); + this.gbxKeys.Controls.Add(this.tbxKey2); + this.gbxKeys.Controls.Add(this.cbxManualUpdate); + this.gbxKeys.Controls.Add(this.tbxKey3); + this.gbxKeys.Location = new System.Drawing.Point(241, 6); + this.gbxKeys.Name = "gbxKeys"; + this.gbxKeys.Size = new System.Drawing.Size(153, 128); + this.gbxKeys.TabIndex = 4; + this.gbxKeys.TabStop = false; + this.gbxKeys.Text = "Keys"; + // + // label3 + // + this.label3.AutoSize = true; + this.label3.Location = new System.Drawing.Point(7, 76); + this.label3.Name = "label3"; + this.label3.Size = new System.Drawing.Size(34, 13); + this.label3.TabIndex = 8; + this.label3.Text = "Key3:"; + // + // label2 + // + this.label2.AutoSize = true; + this.label2.Location = new System.Drawing.Point(7, 50); + this.label2.Name = "label2"; + this.label2.Size = new System.Drawing.Size(34, 13); + this.label2.TabIndex = 8; + this.label2.Text = "Key2:"; + // + // label1 + // + this.label1.AutoSize = true; + this.label1.Location = new System.Drawing.Point(7, 24); + this.label1.Name = "label1"; + this.label1.Size = new System.Drawing.Size(34, 13); + this.label1.TabIndex = 8; + this.label1.Text = "Key1:"; + // + // tbxKey1 + // + this.tbxKey1.BackColor = System.Drawing.SystemColors.Control; + this.tbxKey1.Location = new System.Drawing.Point(43, 21); + this.tbxKey1.Name = "tbxKey1"; + this.tbxKey1.ReadOnly = true; + this.tbxKey1.Size = new System.Drawing.Size(48, 20); + this.tbxKey1.TabIndex = 0; + this.tbxKey1.Text = "0xFFFF"; + // + // tbxKey2 + // + this.tbxKey2.BackColor = System.Drawing.SystemColors.Control; + this.tbxKey2.Location = new System.Drawing.Point(43, 47); + this.tbxKey2.Name = "tbxKey2"; + this.tbxKey2.ReadOnly = true; + this.tbxKey2.Size = new System.Drawing.Size(48, 20); + this.tbxKey2.TabIndex = 1; + this.tbxKey2.Text = "0xFFFF"; + // + // tbxKey3 + // + this.tbxKey3.BackColor = System.Drawing.SystemColors.Control; + this.tbxKey3.Location = new System.Drawing.Point(43, 73); + this.tbxKey3.Name = "tbxKey3"; + this.tbxKey3.ReadOnly = true; + this.tbxKey3.Size = new System.Drawing.Size(48, 20); + this.tbxKey3.TabIndex = 2; + this.tbxKey3.Text = "0xFFFF"; + // + // cbxCreateBackup + // + this.cbxCreateBackup.AutoSize = true; + this.cbxCreateBackup.Checked = true; + this.cbxCreateBackup.CheckState = System.Windows.Forms.CheckState.Checked; + this.cbxCreateBackup.Location = new System.Drawing.Point(6, 19); + this.cbxCreateBackup.Name = "cbxCreateBackup"; + this.cbxCreateBackup.Size = new System.Drawing.Size(97, 17); + this.cbxCreateBackup.TabIndex = 0; + this.cbxCreateBackup.Text = "Create Backup"; + this.toolTip.SetToolTip(this.cbxCreateBackup, "Creates a backup before any modifications will be applied."); + this.cbxCreateBackup.UseVisualStyleBackColor = true; + // + // gbxOptions + // + this.gbxOptions.Controls.Add(this.cbxSkipClientVersion); + this.gbxOptions.Controls.Add(this.cbxSkipKOValidation); + this.gbxOptions.Controls.Add(this.cbxCreateBackup); + this.gbxOptions.Location = new System.Drawing.Point(403, 6); + this.gbxOptions.Name = "gbxOptions"; + this.gbxOptions.Size = new System.Drawing.Size(152, 128); + this.gbxOptions.TabIndex = 5; + this.gbxOptions.TabStop = false; + this.gbxOptions.Text = "Options"; + // + // cbxSkipClientVersion + // + this.cbxSkipClientVersion.AutoSize = true; + this.cbxSkipClientVersion.Location = new System.Drawing.Point(6, 66); + this.cbxSkipClientVersion.Name = "cbxSkipClientVersion"; + this.cbxSkipClientVersion.Size = new System.Drawing.Size(114, 17); + this.cbxSkipClientVersion.TabIndex = 2; + this.cbxSkipClientVersion.Text = "Skip Client Version"; + this.toolTip.SetToolTip(this.cbxSkipClientVersion, "Prevent from the parser to try and retrieve the internal KO client version."); + this.cbxSkipClientVersion.UseVisualStyleBackColor = true; + // + // cbxSkipKOValidation + // + this.cbxSkipKOValidation.AutoSize = true; + this.cbxSkipKOValidation.Location = new System.Drawing.Point(6, 42); + this.cbxSkipKOValidation.Name = "cbxSkipKOValidation"; + this.cbxSkipKOValidation.Size = new System.Drawing.Size(114, 17); + this.cbxSkipKOValidation.TabIndex = 1; + this.cbxSkipKOValidation.Text = "Skip KO Validation"; + this.toolTip.SetToolTip(this.cbxSkipKOValidation, "Prevent from the parser to validate if \"Knight Online Client\" string exists."); + this.cbxSkipKOValidation.UseVisualStyleBackColor = true; + // + // btnViewOffsets + // + this.btnViewOffsets.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right))); + this.btnViewOffsets.Enabled = false; + this.btnViewOffsets.Location = new System.Drawing.Point(241, 181); + this.btnViewOffsets.Name = "btnViewOffsets"; + this.btnViewOffsets.Size = new System.Drawing.Size(153, 32); + this.btnViewOffsets.TabIndex = 1; + this.btnViewOffsets.Text = "View Found Offsets"; + this.toolTip.SetToolTip(this.btnViewOffsets, "View all keys that were found on the target executeable and additional informatio" + + "n on the target exe."); + this.btnViewOffsets.UseVisualStyleBackColor = true; + this.btnViewOffsets.Click += new System.EventHandler(this.btnViewOffsets_Click); + // + // btnUpdateData + // + this.btnUpdateData.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right))); + this.btnUpdateData.Enabled = false; + this.btnUpdateData.Location = new System.Drawing.Point(402, 143); + this.btnUpdateData.Name = "btnUpdateData"; + this.btnUpdateData.Size = new System.Drawing.Size(153, 32); + this.btnUpdateData.TabIndex = 3; + this.btnUpdateData.Text = "Update Data Encryption"; + this.toolTip.SetToolTip(this.btnUpdateData, "Batch update for all current client TBLs to match with the new client encryption"); + this.btnUpdateData.UseVisualStyleBackColor = true; + this.btnUpdateData.Click += new System.EventHandler(this.btnUpdateData_Click); + // + // panelDragArea + // + this.panelDragArea.AllowDrop = true; + this.panelDragArea.BackgroundImage = global::KO.TBLCryptoEditor.Properties.Resources.ko4life; + this.panelDragArea.BackgroundImageLayout = System.Windows.Forms.ImageLayout.Stretch; + this.panelDragArea.Location = new System.Drawing.Point(9, 12); + this.panelDragArea.Name = "panelDragArea"; + this.panelDragArea.Size = new System.Drawing.Size(226, 201); + this.panelDragArea.TabIndex = 5; + // + // MainWindow + // + this.AllowDrop = true; + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.ClientSize = new System.Drawing.Size(566, 245); + this.Controls.Add(this.gbxOptions); + this.Controls.Add(this.gbxKeys); + this.Controls.Add(this.statusStrip); + this.Controls.Add(this.panelDragArea); + this.Controls.Add(this.btnViewOffsets); + this.Controls.Add(this.btnUpdateData); + this.Controls.Add(this.btnUpdateClient); + this.Controls.Add(this.btnGenerateKeys); + this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedSingle; + this.Icon = global::KO.TBLCryptoEditor.Properties.Resources.app; + this.MaximizeBox = false; + this.MinimizeBox = false; + this.Name = "MainWindow"; + this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen; + this.Text = "KnightOnline Table Encryption Editor"; + this.Load += new System.EventHandler(this.MainWindow_Load); + this.statusStrip.ResumeLayout(false); + this.statusStrip.PerformLayout(); + this.gbxKeys.ResumeLayout(false); + this.gbxKeys.PerformLayout(); + this.gbxOptions.ResumeLayout(false); + this.gbxOptions.PerformLayout(); + this.ResumeLayout(false); + this.PerformLayout(); + + } + + #endregion + private System.Windows.Forms.Button btnGenerateKeys; + private System.Windows.Forms.Button btnUpdateClient; + private Controls.CryptoKeyInput tbxKey1; + private Controls.CryptoKeyInput tbxKey2; + private Controls.CryptoKeyInput tbxKey3; + private System.Windows.Forms.Panel panelDragArea; + private System.Windows.Forms.StatusStrip statusStrip; + private System.Windows.Forms.ToolStripStatusLabel lblStatus; + private System.Windows.Forms.CheckBox cbxManualUpdate; + private System.Windows.Forms.GroupBox gbxKeys; + private System.Windows.Forms.CheckBox cbxCreateBackup; + private System.Windows.Forms.GroupBox gbxOptions; + private System.Windows.Forms.Label label3; + private System.Windows.Forms.Label label2; + private System.Windows.Forms.Label label1; + private System.Windows.Forms.ToolTip toolTip; + private System.Windows.Forms.Button btnViewOffsets; + private System.Windows.Forms.CheckBox cbxSkipKOValidation; + private System.Windows.Forms.CheckBox cbxSkipClientVersion; + private System.Windows.Forms.Button btnUpdateData; + } +} + diff --git a/source/KO-TBLCryptoEditor/Views/MainWindow.cs b/source/KO-TBLCryptoEditor/Views/MainWindow.cs new file mode 100644 index 0000000..d598ae3 --- /dev/null +++ b/source/KO-TBLCryptoEditor/Views/MainWindow.cs @@ -0,0 +1,274 @@ +using System; +using System.IO; +using System.Drawing; +using System.Linq; +using System.Windows.Forms; + +using Microsoft.WindowsAPICodePack.Dialogs; + +using KO.TBLCryptoEditor.Controls; +using KO.TBLCryptoEditor.Core; +using KO.TBLCryptoEditor.Utils; + +namespace KO.TBLCryptoEditor.Views +{ + public partial class MainWindow : Form + { + private TargetPE _targetFile; + private short[] _previousKeys; + + public MainWindow() + { + InitializeComponent(); + _targetFile = null; + _previousKeys = null; + } + + private void MainWindow_Load(object sender, EventArgs e) + { + panelDragArea.DragLeave += (ss, ee) => panelDragArea.BackColor = SystemColors.Control; + panelDragArea.DragEnter += (ss, ee) => panelDragArea.BackColor = SystemColors.ActiveCaption; + panelDragArea.DragDrop += (ss, ee) => LoadPE(((string[])ee.Data.GetData(DataFormats.FileDrop))[0]); + panelDragArea.DragEnter += (ss, ee) => { + if (ee.Data.GetDataPresent(DataFormats.FileDrop)) + { + panelDragArea.BackColor = SystemColors.ActiveCaption; + ee.Effect = DragDropEffects.Copy; + } + }; + } + + private void EnableControls() + { + btnGenerateKeys.Enabled = true; + btnUpdateClient.Enabled = true; + cbxManualUpdate.Enabled = true; + btnViewOffsets.Enabled = true; + btnUpdateData.Enabled = true; + //gbxOptions.Controls.Cast().ForEach(c => c.Enabled = true); + } + + private void DisableControls() + { + btnGenerateKeys.Enabled = false; + btnUpdateClient.Enabled = false; + btnViewOffsets.Enabled = false; + cbxManualUpdate.Enabled = false; + btnViewOffsets.Enabled = false; + btnUpdateData.Enabled = false; + //gbxOptions.Controls.Cast().ForEach(c => c.Enabled = false); + } + + private void LoadPE(string filePath) + { + panelDragArea.BackColor = SystemColors.Control; + Action failed = (msg, title) => + { + DisableControls(); + MessageBox.Show(msg, title, MessageBoxButtons.OK, MessageBoxIcon.Error); + lblStatus.Text = "Waiting for user action."; + tbxKey1.Text = tbxKey2.Text = tbxKey3.Text = "0xFFFF"; + }; + + _targetFile = new TargetPE(filePath); + if (!_targetFile.IsValid) + { + failed("Invalid File Path or Extension.", "Invalid File"); + return; + } + + var result = _targetFile.Initialize(cbxSkipKOValidation.Checked, cbxSkipClientVersion.Checked); + if (result != null) + { + failed("Failed to initialize target executeable. Reason:\n" + result.Reason, "Invalid File"); + return; + } + + tbxKey1.Text = tbxKey2.Text = tbxKey3.Text = "0x"; + var samplePatch = _targetFile.CryptoPatches.Find(p => !p.Inlined); + tbxKey1.Text += samplePatch[0].Key.ToString("X4"); + tbxKey2.Text += samplePatch[1].Key.ToString("X4"); + tbxKey3.Text += samplePatch[2].Key.ToString("X4"); + + _previousKeys = new short[CryptoPatch.KEYS_COUNT]; + for (int i = 0; i < CryptoPatch.KEYS_COUNT; i++) + _previousKeys[i] = (short)samplePatch.Keys[i].Key; + + tbxKey2.Enabled = _targetFile.CanUpdateKey2; + + lblStatus.Text = $"[v{_targetFile.ClientVersion}]: Loaded: {filePath}"; + EnableControls(); + + // DES encryption won't be supported for batch upadting client tbls. + btnUpdateData.Enabled = _targetFile.CryptoPatches.CryptoType == CryptoType.XOR; + } + + private void btnGenerateKeys_Click(object sender, EventArgs e) + { + Random rnd = new Random((int)DateTime.Now.Ticks); + tbxKey1.Text = tbxKey3.Text = "0x"; + tbxKey1.Text += rnd.Next(200, 4094).ToString("X4"); + if (tbxKey2.Enabled) + tbxKey2.Text = "0x" + rnd.Next(1000, 310500).ToString("X4"); + tbxKey3.Text += rnd.Next(3500, 614000).ToString("X4"); + } + + private void btnPatchClient_Click(object sender, EventArgs e) + { + if (cbxCreateBackup.Checked && !_targetFile.CreateBackup()) + { + DialogResult answer = MessageBox.Show("Failed to create backup.\n" + + "Would you like to continue anyway?", "Warning", + MessageBoxButtons.YesNo, MessageBoxIcon.Warning); + if (answer == DialogResult.No) + return; + } + + if (Utility.IsFileLocked(_targetFile.FilePath)) + { + MessageBox.Show("Target file seem to be locked by other processes.\n" + + "Make sure to close the following apps and try again:\n" + Utility.WhoIsFileLocking(_targetFile.FilePath), + "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); + return; + } + + if (_targetFile.Patch(tbxKey1.Key, tbxKey2.Key, tbxKey3.Key)) + { + MessageBox.Show("Successfully patched new encryption keys.\n" + + "TBL_Key.txt file saved under the same directory.\n" + + "Next step, make sure to click the 'Update Data Encryption' button, so your client can load them properly.", + "Success", MessageBoxButtons.OK, MessageBoxIcon.Information); + } + else + { + MessageBox.Show("Failed to patch encryption.\n" + + "Please view log file in the same directory and report if necessary.", + "Failed to Update Encryption", MessageBoxButtons.OK, MessageBoxIcon.Error); + } + } + + private void cbxManualUpdate_CheckedChanged(object sender, EventArgs e) + { + if (!_targetFile.IsValid) + return; + + tbxKey1.ReadOnly = !cbxManualUpdate.Checked; + tbxKey2.ReadOnly = !cbxManualUpdate.Checked && _targetFile.CanUpdateKey2; + tbxKey3.ReadOnly = !cbxManualUpdate.Checked; + } + + private void btnViewOffsets_Click(object sender, EventArgs e) + { + new ViewOffsetsWindow(_targetFile).ShowDialog(this); + } + + private void btnUpdateData_Click(object sender, EventArgs e) + { + string targetRootDir = _targetFile.FileInfo.DirectoryName; + string targetDataDir = Path.Combine(targetRootDir, "Data"); + string dirPath = String.Empty; + if (Directory.Exists(targetDataDir)) + { + var answer = MessageBox.Show($"Found Data directory in:\n{targetDataDir}\n" + + $"Would you like to use it to update your current tables encryption? If not, select different directory.", + "Question", MessageBoxButtons.YesNoCancel, MessageBoxIcon.Exclamation); + if (answer == DialogResult.Cancel) + return; + + if (answer == DialogResult.No) + { + CommonOpenFileDialog dialog = new CommonOpenFileDialog(); + dialog.InitialDirectory = targetRootDir; + dialog.IsFolderPicker = true; + if (dialog.ShowDialog() != CommonFileDialogResult.Ok) + return; + + dirPath = dialog.FileName; + } + else if (answer == DialogResult.Yes) + dirPath = targetDataDir; + } + else + { + CommonOpenFileDialog dialog = new CommonOpenFileDialog(); + dialog.InitialDirectory = targetRootDir; + dialog.IsFolderPicker = true; + if (dialog.ShowDialog() != CommonFileDialogResult.Ok) + return; + + dirPath = dialog.FileName; + } + + if (UpdateTablesEncryption(dirPath)) + MessageBox.Show("Successfully update tables to new encryption.", "Success", MessageBoxButtons.OK, MessageBoxIcon.Information); + else + MessageBox.Show("Failed to update tables to the new encryption.\n" + + "Your current TBLs encryption mistmatched with the firstly loaded executeable.", + "Failed", MessageBoxButtons.OK, MessageBoxIcon.Error); + + } + + private bool UpdateTablesEncryption(string dirPath) + { + DirectoryInfo di = new DirectoryInfo(dirPath); + if (cbxCreateBackup.Checked) + { + int iBackupCount = 1; + string destDirTmp; + do + { + destDirTmp = $"{di.FullName}_bak{iBackupCount++}"; + if (!Directory.Exists(destDirTmp)) + { + Directory.CreateDirectory(destDirTmp); + di.CopyFilesRecursively(new DirectoryInfo(destDirTmp)); + break; + } + } while (Directory.Exists(destDirTmp)); + } + + // Slander.tbl gets encrypted separately from the rest of the tbls, so let's keep it as is, as we don't need to update it. + var files = di.GetFiles("*.tbl", SearchOption.TopDirectoryOnly) + .Where(f => !f.Name.StartsWith("Slander", StringComparison.OrdinalIgnoreCase)); + + foreach (var fi in files) + { + byte[] data = File.ReadAllBytes(fi.FullName); + if (IsTableDecrypted(data)) + Console.WriteLine($"[MainWindow::UpdateTablesEncryption]: Table {fi.Name} is not encrypted. Updating to new encryption."); + else + FileSecurity.DecryptXOR(data, _previousKeys[0], _previousKeys[1], _previousKeys[2]); + + FileSecurity.EncryptXOR(data, tbxKey1.Key, tbxKey2.Key, tbxKey3.Key); + File.WriteAllBytes(fi.FullName, data); + } + + return true; + } + + private bool IsTableDecrypted(byte[] data) + { + int readIndex = 0; + const int sizeOfDataType = 4; + int dataTypeCount = BitConverter.ToInt32(data, readIndex); + if (dataTypeCount < 0 || dataTypeCount * sizeOfDataType > data.Length) + return false; + + readIndex += 4; + const int testCount = 5; + for (int i = 0; i < dataTypeCount; i++, readIndex += 4) + { + if (i >= testCount) + break; + + int iDataType = BitConverter.ToInt32(data, readIndex); + // enum DATA_TYPE {DT_NONE, DT_CHAR, DT_BYTE, DT_SHORT, DT_WORD, DT_INT, DT_DWORD, DT_STRING, DT_FLOAT, DT_DOUBLE}; + bool IfWeGetSomeNoneSenseValueReadMustBeEncrypted = iDataType > 50 || iDataType < 0; + if (IfWeGetSomeNoneSenseValueReadMustBeEncrypted) + return false; + } + + return true; + } + } +} diff --git a/source/KO-TBLCryptoEditor/Views/MainWindow.resx b/source/KO-TBLCryptoEditor/Views/MainWindow.resx new file mode 100644 index 0000000..e0ac060 --- /dev/null +++ b/source/KO-TBLCryptoEditor/Views/MainWindow.resx @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 126, 17 + + + 17, 17 + + \ No newline at end of file diff --git a/source/KO-TBLCryptoEditor/Views/ViewOffsetsWindow.Designer.cs b/source/KO-TBLCryptoEditor/Views/ViewOffsetsWindow.Designer.cs new file mode 100644 index 0000000..b10d740 --- /dev/null +++ b/source/KO-TBLCryptoEditor/Views/ViewOffsetsWindow.Designer.cs @@ -0,0 +1,157 @@ +namespace KO.TBLCryptoEditor.Views +{ + partial class ViewOffsetsWindow + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.splitContainer = new System.Windows.Forms.SplitContainer(); + this.view = new System.Windows.Forms.WebBrowser(); + this.layoutButtons = new System.Windows.Forms.TableLayoutPanel(); + this.btnCopyClipboard = new System.Windows.Forms.Button(); + this.btnExit = new System.Windows.Forms.Button(); + this.btnSaveToFile = new System.Windows.Forms.Button(); + ((System.ComponentModel.ISupportInitialize)(this.splitContainer)).BeginInit(); + this.splitContainer.Panel1.SuspendLayout(); + this.splitContainer.Panel2.SuspendLayout(); + this.splitContainer.SuspendLayout(); + this.layoutButtons.SuspendLayout(); + this.SuspendLayout(); + // + // splitContainer + // + this.splitContainer.Dock = System.Windows.Forms.DockStyle.Fill; + this.splitContainer.IsSplitterFixed = true; + this.splitContainer.Location = new System.Drawing.Point(0, 0); + this.splitContainer.Name = "splitContainer"; + this.splitContainer.Orientation = System.Windows.Forms.Orientation.Horizontal; + // + // splitContainer.Panel1 + // + this.splitContainer.Panel1.Controls.Add(this.view); + this.splitContainer.Panel1.RightToLeft = System.Windows.Forms.RightToLeft.No; + // + // splitContainer.Panel2 + // + this.splitContainer.Panel2.Controls.Add(this.layoutButtons); + this.splitContainer.Panel2.RightToLeft = System.Windows.Forms.RightToLeft.No; + this.splitContainer.Size = new System.Drawing.Size(876, 515); + this.splitContainer.SplitterDistance = 430; + this.splitContainer.TabIndex = 1; + // + // view + // + this.view.Dock = System.Windows.Forms.DockStyle.Fill; + this.view.Location = new System.Drawing.Point(0, 0); + this.view.MinimumSize = new System.Drawing.Size(20, 20); + this.view.Name = "view"; + this.view.Size = new System.Drawing.Size(876, 430); + this.view.TabIndex = 0; + // + // layoutButtons + // + this.layoutButtons.ColumnCount = 3; + this.layoutButtons.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 33.33333F)); + this.layoutButtons.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 33.33333F)); + this.layoutButtons.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 33.33333F)); + this.layoutButtons.Controls.Add(this.btnCopyClipboard, 0, 0); + this.layoutButtons.Controls.Add(this.btnExit, 2, 0); + this.layoutButtons.Controls.Add(this.btnSaveToFile, 1, 0); + this.layoutButtons.Dock = System.Windows.Forms.DockStyle.Fill; + this.layoutButtons.Location = new System.Drawing.Point(0, 0); + this.layoutButtons.Margin = new System.Windows.Forms.Padding(0); + this.layoutButtons.Name = "layoutButtons"; + this.layoutButtons.RowCount = 1; + this.layoutButtons.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 100F)); + this.layoutButtons.Size = new System.Drawing.Size(876, 81); + this.layoutButtons.TabIndex = 0; + // + // btnCopyClipboard + // + this.btnCopyClipboard.Dock = System.Windows.Forms.DockStyle.Fill; + this.btnCopyClipboard.Location = new System.Drawing.Point(1, 1); + this.btnCopyClipboard.Margin = new System.Windows.Forms.Padding(1); + this.btnCopyClipboard.Name = "btnCopyClipboard"; + this.btnCopyClipboard.Size = new System.Drawing.Size(290, 79); + this.btnCopyClipboard.TabIndex = 0; + this.btnCopyClipboard.Text = "Copy to Clipboard"; + this.btnCopyClipboard.UseVisualStyleBackColor = true; + this.btnCopyClipboard.Click += new System.EventHandler(this.btnCopyToClipboard_Click); + // + // btnExit + // + this.btnExit.Dock = System.Windows.Forms.DockStyle.Fill; + this.btnExit.Location = new System.Drawing.Point(585, 1); + this.btnExit.Margin = new System.Windows.Forms.Padding(1); + this.btnExit.Name = "btnExit"; + this.btnExit.Size = new System.Drawing.Size(290, 79); + this.btnExit.TabIndex = 0; + this.btnExit.Text = "Exit"; + this.btnExit.UseVisualStyleBackColor = true; + this.btnExit.Click += new System.EventHandler(this.btnExit_Click); + // + // btnSaveToFile + // + this.btnSaveToFile.Dock = System.Windows.Forms.DockStyle.Fill; + this.btnSaveToFile.Location = new System.Drawing.Point(293, 1); + this.btnSaveToFile.Margin = new System.Windows.Forms.Padding(1); + this.btnSaveToFile.Name = "btnSaveToFile"; + this.btnSaveToFile.Size = new System.Drawing.Size(290, 79); + this.btnSaveToFile.TabIndex = 0; + this.btnSaveToFile.Text = "Save to File"; + this.btnSaveToFile.UseVisualStyleBackColor = true; + this.btnSaveToFile.Click += new System.EventHandler(this.btnSaveToFile_Click); + // + // ViewOffsetsWindow + // + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.ClientSize = new System.Drawing.Size(876, 515); + this.Controls.Add(this.splitContainer); + this.Icon = global::KO.TBLCryptoEditor.Properties.Resources.app; + this.MinimumSize = new System.Drawing.Size(320, 336); + this.Name = "ViewOffsetsWindow"; + this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen; + this.Text = "View Offsets"; + this.Load += new System.EventHandler(this.ViewOffsetsWindow_Load); + this.splitContainer.Panel1.ResumeLayout(false); + this.splitContainer.Panel2.ResumeLayout(false); + ((System.ComponentModel.ISupportInitialize)(this.splitContainer)).EndInit(); + this.splitContainer.ResumeLayout(false); + this.layoutButtons.ResumeLayout(false); + this.ResumeLayout(false); + + } + + #endregion + private System.Windows.Forms.SplitContainer splitContainer; + private System.Windows.Forms.TableLayoutPanel layoutButtons; + private System.Windows.Forms.Button btnExit; + private System.Windows.Forms.Button btnCopyClipboard; + private System.Windows.Forms.Button btnSaveToFile; + private System.Windows.Forms.WebBrowser view; + } +} \ No newline at end of file diff --git a/source/KO-TBLCryptoEditor/Views/ViewOffsetsWindow.cs b/source/KO-TBLCryptoEditor/Views/ViewOffsetsWindow.cs new file mode 100644 index 0000000..89f4492 --- /dev/null +++ b/source/KO-TBLCryptoEditor/Views/ViewOffsetsWindow.cs @@ -0,0 +1,160 @@ +using System; +using System.Linq; +using System.Text; +using System.Windows.Forms; + +using KO.TBLCryptoEditor.Core; +using KO.TBLCryptoEditor.Utils; + +namespace KO.TBLCryptoEditor.Views +{ + public partial class ViewOffsetsWindow : Form + { + private TargetPE _pe; + + public ViewOffsetsWindow(TargetPE pe) + { + if (pe == null) + throw new ArgumentNullException(); + + _pe = pe; + + InitializeComponent(); + } + + private void ViewOffsetsWindow_Load(object sender, EventArgs e) + { + SerializePatches(); + } + + private void SerializePatches() + { + StringBuilder sb = new StringBuilder(); + sb.AppendLine($""); + + var patches = _pe.CryptoPatches; + int patCount = patches.Count; + AddLineWithTag(sb, "Patches Count", patCount); + AddLineWithTag(sb, "Target Executeable", _pe.FilePath); + AddLineWithTag(sb, "Client Internal Version", _pe.ClientVersion); + AddLineWithTag(sb, "Linker Version", $"MSVC-{_pe.MajorLinkerVersion}.0 (Microsoft Visual C++)"); + AddLineWithTag(sb, "Inlined Functions Count", patches.Count(p => p.Inlined)); + + string encryptionDesc = patches.CryptoType == CryptoType.XOR ? "XOR Cipher" : "DES + XOR Cipher"; + AddLineWithTag(sb, "Encryption Algorithm", encryptionDesc); + + AddSeparator(sb); + + using (HtmlTable table = new HtmlTable(sb, "Table Encryption Keys")) + { + using (HtmlTableRow row = new HtmlTableRow(sb, true)) + { + row.AddCell("#"); + row.AddCell("Key"); + row.AddCell("File Offset"); + row.AddCell("Virtual Address"); + row.AddCell("Inlined Function?"); + } + + for (int i = 0; i < patCount; i++) + { + using (HtmlTableRow row = new HtmlTableRow(sb)) + { + CryptoPatch patch = patches[i]; + CryptoKey fk = patch.Keys.First(); + + row.AddCell((i+1).ToString()); + + string keys, fileOffsets, VAs; + keys = fileOffsets = VAs = String.Empty; + foreach (CryptoKey key in patch.Keys) + { + keys += $"• 0x{key.Key:X4}
\n"; + fileOffsets += $"• 0x{((int)key.Offset):X8}
\n"; + VAs += $"• 0x{((int)key.VirtualAddress):X8}
\n"; + } + keys.TrimEnd('\n'); + fileOffsets.TrimEnd('\n'); + VAs.TrimEnd('\n'); + + row.AddCell(keys); + row.AddCell(fileOffsets); + row.AddCell(VAs); + row.AddCell(patch.Inlined.ToString()); + } + } + } + + view.DocumentText = sb.ToString(); + } + + private void AddLineWithTag(StringBuilder sb, string tag, int value) + { + AddLineWithTag(sb, tag, value.ToString()); + } + + private void AddLineWithTag(StringBuilder sb, string tag, string text) + { + AddLine(sb, $"{tag}: {text}"); + } + + private void AddLine(StringBuilder sb, string text) + { + sb.AppendLine($"{text}
"); + } + + private void AddSeparator(StringBuilder sb) + { + sb.AppendLine("

"); + } + + private void btnExit_Click(object sender, EventArgs e) + { + Close(); + } + + private void btnSaveToFile_Click(object sender, EventArgs e) + { + // TODO(Gilad): Implement. + } + + private void btnCopyToClipboard_Click(object sender, EventArgs e) + { + // TODO(Gilad): Implement. + } + + private string GetStyleSheet() + { + return @" +caption { + font-size: 25px; + font-weight: bold; + padding: 5px; +} + +table { + font-family: ""Trebuchet MS"", Arial, Helvetica, sans-serif; + border-collapse: collapse; + width: 100%; +} + +table td, table th { + border: 1px solid #ddd; + padding: 8px; +} + +table tr:nth-child(even){background-color: #f2f2f2;} + +table tr:hover {background-color: #ddd;} + +table th { + padding-top: 12px; + padding-bottom: 12px; + text-align: left; + background-color: #4CAF50; + color: white; +} + "; + } + } +} diff --git a/source/KO-TBLCryptoEditor/Views/ViewOffsetsWindow.resx b/source/KO-TBLCryptoEditor/Views/ViewOffsetsWindow.resx new file mode 100644 index 0000000..1af7de1 --- /dev/null +++ b/source/KO-TBLCryptoEditor/Views/ViewOffsetsWindow.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/source/KO-TBLCryptoEditor/packages.config b/source/KO-TBLCryptoEditor/packages.config new file mode 100644 index 0000000..a771189 --- /dev/null +++ b/source/KO-TBLCryptoEditor/packages.config @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/thirdparty/PatternFinder b/thirdparty/PatternFinder new file mode 160000 index 0000000..6fec069 --- /dev/null +++ b/thirdparty/PatternFinder @@ -0,0 +1 @@ +Subproject commit 6fec069701aca611234af40330d40b323b79e8f3 diff --git a/thirdparty/pe b/thirdparty/pe new file mode 160000 index 0000000..e6ac8a5 --- /dev/null +++ b/thirdparty/pe @@ -0,0 +1 @@ +Subproject commit e6ac8a56cb99515ccdf11255f188d0c828863023