From 16c0daf643820618d84d161fe700daf4c25eebfc Mon Sep 17 00:00:00 2001 From: morkt Date: Wed, 22 Feb 2017 19:32:02 +0400 Subject: [PATCH] (QLIE): attempt to extract archive key from game EXE resources. --- ArcFormats/ArcFormats.csproj | 1 + ArcFormats/Qlie/ArcQLIE.cs | 62 ++++++++++- ArcFormats/Qlie/DelphiDeserializer.cs | 147 ++++++++++++++++++++++++++ 3 files changed, 205 insertions(+), 5 deletions(-) create mode 100644 ArcFormats/Qlie/DelphiDeserializer.cs diff --git a/ArcFormats/ArcFormats.csproj b/ArcFormats/ArcFormats.csproj index 32c7c8a6..fc065a39 100644 --- a/ArcFormats/ArcFormats.csproj +++ b/ArcFormats/ArcFormats.csproj @@ -97,6 +97,7 @@ + diff --git a/ArcFormats/Qlie/ArcQLIE.cs b/ArcFormats/Qlie/ArcQLIE.cs index b8d241ad..d6e9c028 100644 --- a/ArcFormats/Qlie/ArcQLIE.cs +++ b/ArcFormats/Qlie/ArcQLIE.cs @@ -2,7 +2,7 @@ //! \date Mon Jun 15 04:03:18 2015 //! \brief QLIE engine archives implementation. // -// Copyright (C) 2015 by morkt +// Copyright (C) 2015-2017 by morkt // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to @@ -32,6 +32,7 @@ using System.Linq; using GameRes.Utility; using GameRes.Formats.Properties; using GameRes.Formats.Strings; +using GameRes.Formats.Borland; namespace GameRes.Formats.Qlie { @@ -136,7 +137,7 @@ namespace GameRes.Formats.Qlie use_pack_keyfile = key_file != null; // currently, user is prompted to choose encryption scheme only if there's 'key.fkey' file found. if (use_pack_keyfile) - arc_key = QueryEncryption(); + arc_key = QueryEncryption (file); // use_pack_keyfile = null != arc_key; var key_data = file.View.ReadBytes (file.MaxOffset-0x41C, 0x100); @@ -423,10 +424,20 @@ namespace GameRes.Formats.Qlie return new GUI.WidgetQLIE(); } - byte[] QueryEncryption () + byte[] QueryEncryption (ArcView file) { - var options = Query (arcStrings.ArcEncryptedNotice); - return options.GameKeyData; + var title = FormatCatalog.Instance.LookupGame (file.Name, @"..\*.exe"); + byte[] key = null; + if (!string.IsNullOrEmpty (title)) + key = GetKeyData (title); + if (null == key) + key = GuessKeyData (file.Name); + if (null == key) + { + var options = Query (arcStrings.ArcEncryptedNotice); + key = options.GameKeyData; + } + return key; } static byte[] GetKeyData (string scheme) @@ -457,5 +468,46 @@ namespace GameRes.Formats.Qlie } return null; } + + byte[] GuessKeyData (string arc_name) + { + if (VFS.IsVirtual) + return null; + // XXX add button to query dialog like with CatSystem? + var pattern = VFS.CombinePath (VFS.GetDirectoryName (arc_name), @"..\*.exe"); + foreach (var file in VFS.GetFiles (pattern)) + { + try + { + var key = GetKeyDataFromExe (file.Name); + if (key != null) + return key; + } + catch { /* ignore errors */ } + } + return null; + } + + public static byte[] GetKeyDataFromExe (string filename) + { + using (var exe = new ExeFile.ResourceAccessor (filename)) + { + var tform = exe.GetResource ("TFORM1", "#10"); + if (null == tform || !tform.AsciiEqual (0, "TPF0")) + return null; + using (var input = new BinMemoryStream (tform)) + { + var deserializer = new DelphiDeserializer (input); + var form = deserializer.Deserialize(); + var image = form.Contents.FirstOrDefault (n => n.Name == "IconKeyImage"); + if (null == image) + return null; + var icon = image.Props["Picture.Data"] as byte[]; + if (null == icon || icon.Length < 0x106 || !icon.AsciiEqual (0, "\x05TIcon")) + return null; + return new CowArray (icon, 6, 0x100).ToArray(); + } + } + } } } diff --git a/ArcFormats/Qlie/DelphiDeserializer.cs b/ArcFormats/Qlie/DelphiDeserializer.cs new file mode 100644 index 00000000..3cbe7983 --- /dev/null +++ b/ArcFormats/Qlie/DelphiDeserializer.cs @@ -0,0 +1,147 @@ +//! \file DelphiDeserializer.cs +//! \date Wed Feb 22 15:40:33 2017 +//! \brief Borland Delphi binary data deserializer. +// +// Copyright (C) 2017 by morkt +// +// 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. +// + +using System.Collections; +using System.Collections.Generic; +using System.Text; + +namespace GameRes.Formats.Borland +{ + public sealed class DelphiDeserializer + { + IBinaryStream m_input; + + public Encoding Encoding { get; set; } + + public DelphiDeserializer (IBinaryStream input) + { + m_input = input; + Encoding = Encodings.cp932; + } + + public DelphiObject Deserialize () + { + if (m_input.ReadUInt32() != 0x30465054) // 'TPF0' + return null; + return DeserializeNode(); + } + + DelphiObject DeserializeNode () + { + int type_len = m_input.ReadByte(); + if (type_len <= 0) + return null; + var node = new DelphiObject(); + node.Type = ReadString (type_len); + node.Name = ReadString(); + int key_length; + while ((key_length = m_input.ReadUInt8()) > 0) + { + var key = ReadString (key_length); + node.Props[key] = ReadValue(); + } + DelphiObject child; + while ((child = DeserializeNode()) != null) + { + node.Contents.Add (child); + } + return node; + } + + object ReadValue () + { + int type = m_input.ReadUInt8(); + switch (type) + { + case 2: return (int)m_input.ReadUInt8(); + case 3: return (int)m_input.ReadUInt16(); + case 5: return ReadLongDouble(); + case 6: + case 7: return ReadString(); + case 8: + case 9: return true; + case 10: return ReadByteString(); + case 11: return ReadStringArray(); + case 18: return ReadUnicodeString(); + default: throw new System.NotImplementedException(); + } + } + + string ReadString () + { + return ReadString (m_input.ReadUInt8()); + } + + string ReadString (int length) + { + return m_input.ReadCString (length, Encoding); + } + + string ReadUnicodeString () + { + int length = m_input.ReadInt32(); + if (length < 0) + throw new InvalidFormatException(); + if (0 == length) + return ""; + var bytes = m_input.ReadBytes (length * 2); + return Encoding.Unicode.GetString (bytes); + } + + byte[] ReadByteString () + { + int length = m_input.ReadInt32(); + if (length < 0) + throw new InvalidFormatException(); + if (0 == length) + return new byte[0]; + return m_input.ReadBytes (length); + } + + IList ReadStringArray () + { + var list = new List(); + int length; + while ((length = m_input.ReadUInt8()) > 0) + { + list.Add (ReadString (length)); + } + return list; + } + + object ReadLongDouble () + { + return m_input.ReadBytes (10); // long double deserialization not implemented + } + } + + public class DelphiObject + { + public string Type; + public string Name; + public IDictionary Props = new Hashtable(); + public IList Contents = new List(); + } +}