//! \file KiriKiriCx.cs //! \date Sun Sep 07 06:50:11 2014 //! \brief KiriKiri Cx encryption scheme implementation. // // Copyright (C) 2014-2016 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; using System.Collections.Generic; using System.Diagnostics; using System.Runtime.Serialization; using System.Text; namespace GameRes.Formats.KiriKiri { public class CxProgramException : ApplicationException { public CxProgramException (string message) : base (message) { } } [Serializable] public class CxScheme { public uint Mask; public uint Offset; public byte[] PrologOrder; public byte[] OddBranchOrder; public byte[] EvenBranchOrder; public uint[] ControlBlock; public string TpmFileName; } [Serializable] public class CxEncryption : ICrypt { private uint m_mask; private uint m_offset; protected byte[] PrologOrder; protected byte[] OddBranchOrder; protected byte[] EvenBranchOrder; protected uint[] ControlBlock; protected string TpmFileName; [NonSerialized] CxProgram[] m_program_list = new CxProgram[0x80]; [OnDeserialized()] void PostDeserialization (StreamingContext context) { m_program_list = new CxProgram[0x80]; } public CxEncryption (CxScheme scheme) { m_mask = scheme.Mask; m_offset = scheme.Offset; PrologOrder = scheme.PrologOrder; OddBranchOrder = scheme.OddBranchOrder; EvenBranchOrder = scheme.EvenBranchOrder; ControlBlock = scheme.ControlBlock; TpmFileName = scheme.TpmFileName; } static readonly byte[] s_ctl_block_signature = Encoding.ASCII.GetBytes (" Encryption control block"); /// /// Look for control block within specified TPM plugin file. /// public override void Init (ArcFile arc) { if (ControlBlock != null) return; if (string.IsNullOrEmpty (TpmFileName)) throw new InvalidEncryptionScheme(); var dir_name = VFS.GetDirectoryName (arc.File.Name); var tpm_name = VFS.CombinePath (dir_name, TpmFileName); using (var tpm = VFS.OpenView (tpm_name)) { if (tpm.MaxOffset < 0x1000 || tpm.MaxOffset > uint.MaxValue) throw new InvalidEncryptionScheme ("Invalid KiriKiri TPM plugin"); using (var view = tpm.CreateViewAccessor (0, (uint)tpm.MaxOffset)) unsafe { byte* begin = view.GetPointer (0); byte* end = begin + (((uint)tpm.MaxOffset - 0x1000u) & ~0xFu); try { while (begin < end) { int i; for (i = 0; i < s_ctl_block_signature.Length; ++i) { if (begin[i] != s_ctl_block_signature[i]) break; } if (s_ctl_block_signature.Length == i) { ControlBlock = new uint[0x400]; uint* src = (uint*)begin; for (i = 0; i < ControlBlock.Length; ++i) ControlBlock[i] = ~src[i]; return; } begin += 0x10; // control block expected to be on a paragraph boundary } throw new InvalidEncryptionScheme ("No control block found inside TPM plugin"); } finally { view.SafeMemoryMappedViewHandle.ReleasePointer(); } } } } uint GetBaseOffset (uint hash) { return (hash & m_mask) + m_offset; } public override byte Decrypt (Xp3Entry entry, long offset, byte value) { uint key = entry.Hash; uint base_offset = GetBaseOffset (key); if (offset >= base_offset) { key = (key >> 16) ^ key; } var buffer = new byte[1] { value }; Decode (key, offset, buffer, 0, 1); return buffer[0]; } public override void Decrypt (Xp3Entry entry, long offset, byte[] buffer, int pos, int count) { uint key = entry.Hash; uint base_offset = GetBaseOffset (key); if (offset < base_offset) { int base_length = Math.Min ((int)(base_offset - offset), count); Decode (key, offset, buffer, pos, base_length); offset += base_length; pos += base_length; count -= base_length; } if (count > 0) { key = (key >> 16) ^ key; Decode (key, offset, buffer, pos, count); } } void Decode (uint key, long offset, byte[] buffer, int pos, int count) { Tuple ret = ExecuteXCode (key); uint key1 = ret.Item2 >> 16; uint key2 = ret.Item2 & 0xffff; byte key3 = (byte)(ret.Item1); if (key1 == key2) key2 = (key2+1) & 0xffff; if (0 == key3) key3 = 1; if ((key2 >= offset) && (key2 < offset + count)) buffer[pos + key2 - offset] ^= (byte)(ret.Item1 >> 16); if ((key1 >= offset) && (key1 < offset + count)) buffer[pos + key1 - offset] ^= (byte)(ret.Item1 >> 8); for (int i = 0; i < count; ++i) buffer[pos + i] ^= key3; } public override void Encrypt (Xp3Entry entry, long offset, byte[] values, int pos, int count) { Decrypt (entry, offset, values, pos, count); } Tuple ExecuteXCode (uint hash) { uint seed = hash & 0x7f; if (null == m_program_list[seed]) { m_program_list[seed] = GenerateProgram (seed); } hash >>= 7; uint ret1 = m_program_list[seed].Execute (hash); uint ret2 = m_program_list[seed].Execute (~hash); return new Tuple (ret1, ret2); } CxProgram GenerateProgram (uint seed) { var program = new CxProgram (seed, ControlBlock); for (int stage = 5; stage > 0; --stage) { if (EmitCode (program, stage)) return program; // Trace.WriteLine (string.Format ("stage {0} failed for seed {1}", stage, seed), "GenerateProgram"); program.Clear(); } throw new CxProgramException ("Overly large CxEncryption bytecode"); } bool EmitCode (CxProgram program, int stage) { return program.EmitNop (5) // 0x57 0x56 0x53 0x51 0x52 && program.Emit (CxByteCode.MOV_EDI_ARG, 4) // 0x8b 0x7c 0x24 0x18 && EmitBody (program, stage) && program.EmitNop (5) // 0x5a 0x59 0x5b 0x5e 0x5f && program.Emit (CxByteCode.RETN); // 0xc3 } bool EmitBody (CxProgram program, int stage) { if (1 == stage) return EmitProlog (program); if (!program.Emit (CxByteCode.PUSH_EBX)) // 0x53 return false; if (0 != (program.GetRandom() & 1)) { if (!EmitBody (program, stage - 1)) return false; } else if (!EmitBody2 (program, stage - 1)) return false; if (!program.Emit (CxByteCode.MOV_EBX_EAX, 2)) // 0x89 0xc3 return false; if (0 != (program.GetRandom() & 1)) { if (!EmitBody (program, stage - 1)) return false; } else if (!EmitBody2 (program, stage - 1)) return false; return EmitOddBranch (program) && program.Emit (CxByteCode.POP_EBX); // 0x5b } bool EmitBody2 (CxProgram program, int stage) { if (1 == stage) return EmitProlog (program); bool rc = true; if (0 != (program.GetRandom() & 1)) rc = EmitBody (program, stage - 1); else rc = EmitBody2 (program, stage - 1); return rc && EmitEvenBranch (program); } bool EmitProlog (CxProgram program) { bool rc = true; switch (PrologOrder[program.GetRandom() % 3]) { case 2: // MOV EAX, (Random() & 0x3ff) // MOV EAX, EncryptionControlBlock[EAX] rc = program.EmitNop (5) // 0xbe && program.Emit (CxByteCode.MOV_EAX_IMMED, 2) // 0x8b 0x86 && program.EmitUInt32 (program.GetRandom() & 0x3ff) && program.Emit (CxByteCode.MOV_EAX_INDIRECT, 0); break; case 1: rc = program.Emit (CxByteCode.MOV_EAX_EDI, 2); // 0x8b 0xc7 break; case 0: // MOV EAX, Random() rc = program.Emit (CxByteCode.MOV_EAX_IMMED) // 0xb8 && program.EmitRandom(); break; } return rc; } bool EmitEvenBranch (CxProgram program) { bool rc = true; switch (EvenBranchOrder[program.GetRandom() & 7]) { case 0: rc = program.Emit (CxByteCode.NOT_EAX, 2); // 0xf7 0xd0 break; case 1: rc = program.Emit (CxByteCode.DEC_EAX); // 0x48 break; case 2: rc = program.Emit (CxByteCode.NEG_EAX, 2); // 0xf7 0xd8 break; case 3: rc = program.Emit (CxByteCode.INC_EAX); // 0x40 break; case 4: rc = program.EmitNop (5) // 0xbe && program.Emit (CxByteCode.AND_EAX_IMMED) // 0x25 && program.EmitUInt32 (0x3ff) && program.Emit (CxByteCode.MOV_EAX_INDIRECT, 3); // 0x8b 0x04 0x86 break; case 5: rc = program.Emit (CxByteCode.PUSH_EBX) // 0x53 && program.Emit (CxByteCode.MOV_EBX_EAX, 2) // 0x89 0xc3 && program.Emit (CxByteCode.AND_EBX_IMMED, 2) // 0x81 0xe3 && program.EmitUInt32 (0xaaaaaaaa) && program.Emit (CxByteCode.AND_EAX_IMMED) // 0x25 && program.EmitUInt32 (0x55555555) && program.Emit (CxByteCode.SHR_EBX_1, 2) // 0xd1 0xeb && program.Emit (CxByteCode.SHL_EAX_1, 2) // 0xd1 0xe0 && program.Emit (CxByteCode.OR_EAX_EBX, 2) // 0x09 0xd8 && program.Emit (CxByteCode.POP_EBX); // 0x5b break; case 6: rc = program.Emit (CxByteCode.XOR_EAX_IMMED) // 0x35 && program.EmitRandom(); break; case 7: if (0 != (program.GetRandom() & 1)) rc = program.Emit (CxByteCode.ADD_EAX_IMMED); // 0x05 else rc = program.Emit (CxByteCode.SUB_EAX_IMMED); // 0x2d rc = rc && program.EmitRandom(); break; } return rc; } bool EmitOddBranch (CxProgram program) { bool rc = true; switch (OddBranchOrder[program.GetRandom() % 6]) { case 0: rc = program.Emit (CxByteCode.PUSH_ECX) // 0x51 && program.Emit (CxByteCode.MOV_ECX_EBX, 2) // 0x89 0xd9 && program.Emit (CxByteCode.AND_ECX_0F, 3) // 0x83 0xe1 0x0f && program.Emit (CxByteCode.SHR_EAX_CL, 2) // 0xd3 0xe8 && program.Emit (CxByteCode.POP_ECX); // 0x59 break; case 1: rc = program.Emit (CxByteCode.PUSH_ECX) // 0x51 && program.Emit (CxByteCode.MOV_ECX_EBX, 2) // 0x89 0xd9 && program.Emit (CxByteCode.AND_ECX_0F, 3) // 0x83 0xe1 0x0f && program.Emit (CxByteCode.SHL_EAX_CL, 2) // 0xd3 0xe0 && program.Emit (CxByteCode.POP_ECX); // 0x59 break; case 2: rc = program.Emit (CxByteCode.ADD_EAX_EBX, 2); // 0x01 0xd8 break; case 3: rc = program.Emit (CxByteCode.NEG_EAX, 2) // 0xf7 0xd8 && program.Emit (CxByteCode.ADD_EAX_EBX, 2); // 0x01 0xd8 break; case 4: rc = program.Emit (CxByteCode.IMUL_EAX_EBX, 3); // 0x0f 0xaf 0xc3 break; case 5: rc = program.Emit (CxByteCode.SUB_EAX_EBX, 2); // 0x29 0xd8 break; } return rc; } } enum CxByteCode { NOP, RETN, MOV_EDI_ARG, PUSH_EBX, POP_EBX, PUSH_ECX, POP_ECX, MOV_EAX_EBX, MOV_EBX_EAX, MOV_ECX_EBX, MOV_EAX_CONTROL_BLOCK, MOV_EAX_EDI, MOV_EAX_INDIRECT, ADD_EAX_EBX, SUB_EAX_EBX, IMUL_EAX_EBX, AND_ECX_0F, SHR_EBX_1, SHL_EAX_1, SHR_EAX_CL, SHL_EAX_CL, OR_EAX_EBX, NOT_EAX, NEG_EAX, DEC_EAX, INC_EAX, IMMED = 0x100, MOV_EAX_IMMED, AND_EBX_IMMED, AND_EAX_IMMED, XOR_EAX_IMMED, ADD_EAX_IMMED, SUB_EAX_IMMED, } internal class CxProgram { public const int LengthLimit = 0x80; private List m_code = new List (LengthLimit); private uint[] m_ControlBlock; private int m_length; private uint m_seed; class Context { public uint eax; public uint ebx; public uint ecx; public uint edi; public Stack stack = new Stack(); } public CxProgram (uint seed, uint[] control_block) { m_seed = seed; m_length = 0; m_ControlBlock = control_block; } public uint Execute (uint hash) { var context = new Context(); using (var iterator = m_code.GetEnumerator()) { uint immed = 0; while (iterator.MoveNext()) { var bytecode = (CxByteCode)iterator.Current; if (CxByteCode.IMMED == (bytecode & CxByteCode.IMMED)) { if (!iterator.MoveNext()) throw new CxProgramException ("Incomplete IMMED bytecode in CxEncryption program"); immed = iterator.Current; } switch (bytecode) { case CxByteCode.NOP: break; case CxByteCode.IMMED: break; case CxByteCode.MOV_EDI_ARG: context.edi = hash; break; case CxByteCode.PUSH_EBX: context.stack.Push (context.ebx); break; case CxByteCode.POP_EBX: context.ebx = context.stack.Pop(); break; case CxByteCode.PUSH_ECX: context.stack.Push (context.ecx); break; case CxByteCode.POP_ECX: context.ecx = context.stack.Pop(); break; case CxByteCode.MOV_EBX_EAX: context.ebx = context.eax; break; case CxByteCode.MOV_EAX_EDI: context.eax = context.edi; break; case CxByteCode.MOV_ECX_EBX: context.ecx = context.ebx; break; case CxByteCode.MOV_EAX_EBX: context.eax = context.ebx; break; case CxByteCode.AND_ECX_0F: context.ecx &= 0x0f; break; case CxByteCode.SHR_EBX_1: context.ebx >>= 1; break; case CxByteCode.SHL_EAX_1: context.eax <<= 1; break; case CxByteCode.SHR_EAX_CL: context.eax >>= (int)context.ecx; break; case CxByteCode.SHL_EAX_CL: context.eax <<= (int)context.ecx; break; case CxByteCode.OR_EAX_EBX: context.eax |= context.ebx; break; case CxByteCode.NOT_EAX: context.eax = ~context.eax; break; case CxByteCode.NEG_EAX: context.eax = (uint)-context.eax; break; case CxByteCode.DEC_EAX: context.eax--; break; case CxByteCode.INC_EAX: context.eax++; break; case CxByteCode.ADD_EAX_EBX: context.eax += context.ebx; break; case CxByteCode.SUB_EAX_EBX: context.eax -= context.ebx; break; case CxByteCode.IMUL_EAX_EBX: context.eax *= context.ebx; break; case CxByteCode.ADD_EAX_IMMED: context.eax += immed; break; case CxByteCode.SUB_EAX_IMMED: context.eax -= immed; break; case CxByteCode.AND_EBX_IMMED: context.ebx &= immed; break; case CxByteCode.AND_EAX_IMMED: context.eax &= immed; break; case CxByteCode.XOR_EAX_IMMED: context.eax ^= immed; break; case CxByteCode.MOV_EAX_IMMED: context.eax = immed; break; case CxByteCode.MOV_EAX_INDIRECT: if (context.eax >= m_ControlBlock.Length) throw new CxProgramException ("Index out of bounds in CxEncryption program"); context.eax = ~m_ControlBlock[context.eax]; break; case CxByteCode.RETN: if (context.stack.Count > 0) throw new CxProgramException ("Imbalanced stack in CxEncryption program"); return context.eax; default: throw new CxProgramException ("Invalid bytecode in CxEncryption program"); } } } throw new CxProgramException ("CxEncryption program without RETN bytecode"); } public void Clear () { m_length = 0; m_code.Clear(); } public bool EmitNop (int count) { if (m_length + count > LengthLimit) return false; m_length += count; return true; } public bool Emit (CxByteCode code, int length = 1) { if (m_length + length > LengthLimit) return false; m_length += length; m_code.Add ((uint)code); return true; } public bool EmitUInt32 (uint x) { if (m_length + 4 > LengthLimit) return false; m_length += 4; m_code.Add (x); return true; } public bool EmitRandom () { return EmitUInt32 (GetRandom()); } public uint GetRandom () { uint seed = m_seed; m_seed = 1103515245 * seed + 12345; return m_seed ^ (seed << 16) ^ (seed >> 16); } } /* CxEncryption base branch order OddBranchOrder { case 0: SHR_EAX_CL case 1: SHL_EAX_CL case 2: ADD_EAX_EBX case 3: NEG_EAX; ADD_EAX_EBX case 4: IMUL_EAX_EBX case 5: SUB_EAX_EBX } EvenBranchOrder { case 0: NOT_EAX case 1: DEC_EAX case 2: NEG_EAX case 3: INC_EAX case 4: MOV_EAX_INDIRECT case 5: OR_EAX_EBX case 6: XOR_EAX_IMMED case 7: ADD_EAX_IMMED } */ }