| r177 vs r182 | ||
|---|---|---|
| ... | ... | |
| 55 | 55 | |
| 56 | 56 | [[/연습장|{{{#!wiki style="display: inline; color: transparent; background: text linear-gradient(to right, #f58abb, #fed09f)" |
| 57 | 57 | '''연습장 바로가기'''}}}]] |
| 58 | ||
| 59 | {{{#!syntax csharp | |
| 60 | using System; | |
| 61 | using System.IO; | |
| 62 | using System.Security.Cryptography; | |
| 63 | using System.Text; | |
| 64 | using UnityEngine; | |
| 65 | using System.Diagnostics; | |
| 66 | using System.Runtime.CompilerServices; | |
| 67 | ||
| 68 | [Serializable] | |
| 69 | public class PlayerData | |
| 70 | { | |
| 71 | public string playerName; | |
| 72 | public int score; | |
| 73 | } | |
| 74 | ||
| 75 | public class SecureManager : MonoBehaviour | |
| 76 | { | |
| 77 | public static SecureManager Instance { get; private set; } | |
| 78 | ||
| 79 | public bool EnableHardening = true; | |
| 80 | public bool EnableAntiDebugChecks = true; | |
| 81 | public bool EnableRootDetection = true; | |
| 82 | public bool EnableDummyMix = true; | |
| 83 | public bool EnableFileNameObfuscation = true; | |
| 84 | public bool EnableRotation = true; | |
| 85 | ||
| 86 | private const string Magic = "SJMP"; | |
| 87 | private const byte Version = 1; | |
| 88 | private const int SaltLenDefault = 12; | |
| 89 | private const int IvLen = 16; | |
| 90 | private const int HmacLen = 32; | |
| 91 | private const int KeyMaterialLen = 64; | |
| 92 | private const int AesKeyLen = 32; | |
| 93 | private const int HmacKeyLen = 32; | |
| 94 | private const int Pbkdf2Iterations = 20000; | |
| 95 | private readonly byte[] obfPartA = new byte[] { 0x4A, 0x5F, 0x33, 0x29, 0x11, 0x7C, 0x6D, 0x3E, 0x2D, 0x7A, 0x5B, 0x1C }; | |
| 96 | private readonly byte[] obfPartB = new byte[] { 0x91, 0x20, 0x55, 0x12, 0x44, 0x38, 0x77, 0x0A, 0x6F, 0x21, 0x9D, 0xEE }; | |
| 97 | private const byte Mask = 0xAA; | |
| 98 | private string filePath; | |
| 99 | private byte[] sessionNonce; | |
| 100 | ||
| 101 | void Awake() | |
| 102 | { | |
| 103 | if (Instance != null && Instance != this) | |
| 104 | { | |
| 105 | Destroy(gameObject); | |
| 106 | return; | |
| 107 | } | |
| 108 | Instance = this; | |
| 109 | DontDestroyOnLoad(gameObject); | |
| 110 | ||
| 111 | sessionNonce = GenerateRandomBytes(16); | |
| 112 | filePath = Path.Combine(Application.persistentDataPath, ResolveFileName("playerData")); | |
| 113 | Warmup(); | |
| 114 | } | |
| 115 | ||
| 116 | private void Warmup() | |
| 117 | { | |
| 118 | try { _ = GetSecretBytes(); } catch { } | |
| 119 | } | |
| 120 | ||
| 121 | [MethodImpl(MethodImplOptions.NoInlining)] | |
| 122 | private byte[] DumbMixA(byte[] input) | |
| 123 | { | |
| 124 | byte[] outb = new byte[input.Length]; | |
| 125 | for (int i = 0; i < input.Length; i++) | |
| 126 | { | |
| 127 | int v = input[i]; | |
| 128 | v = ((v << 3) | (v >> 5)) & 0xFF; | |
| 129 | v ^= (i * 37) & 0xFF; | |
| 130 | outb[i] = (byte)v; | |
| 131 | } | |
| 132 | return outb; | |
| 133 | } | |
| 134 | ||
| 135 | [MethodImpl(MethodImplOptions.NoInlining)] | |
| 136 | private byte[] DumbMixB(byte[] input, int seed) | |
| 137 | { | |
| 138 | byte[] outb = new byte[input.Length]; | |
| 139 | for (int i = 0; i < input.Length; i++) | |
| 140 | { | |
| 141 | int v = input[i] ^ (seed >> (i % 8)); | |
| 142 | v = (v * 13 + (i * 7)) & 0xFF; | |
| 143 | v = (v ^ 0x5A) & 0xFF; | |
| 144 | outb[i] = (byte)v; | |
| 145 | } | |
| 146 | return outb; | |
| 147 | } | |
| 148 | ||
| 149 | [MethodImpl(MethodImplOptions.NoInlining)] | |
| 150 | private byte[] DumbMixC(byte[] input, byte[] nonce) | |
| 151 | { | |
| 152 | byte[] outb = new byte[input.Length]; | |
| 153 | for (int i = 0; i < input.Length; i++) | |
| 154 | { | |
| 155 | int n = nonce[i % nonce.Length]; | |
| 156 | int v = input[i]; | |
| 157 | v = ((v + n) ^ (n >> (i % 4))) & 0xFF; | |
| 158 | outb[i] = (byte)v; | |
| 159 | } | |
| 160 | return outb; | |
| 161 | } | |
| 162 | ||
| 163 | private byte[] GetSecretBytes() | |
| 164 | { | |
| 165 | byte[] k = new byte[obfPartA.Length + obfPartB.Length]; | |
| 166 | for (int i = 0; i < obfPartA.Length; i++) k[i] = (byte)(obfPartA[i] ^ Mask); | |
| 167 | for (int i = 0; i < obfPartB.Length; i++) k[i + obfPartA.Length] = (byte)(obfPartB[i] ^ Mask); | |
| 168 | ||
| 169 | if (!EnableHardening || !EnableDummyMix) | |
| 170 | { | |
| 171 | byte[] simple = new byte[k.Length]; | |
| 172 | Buffer.BlockCopy(k, 0, simple, 0, k.Length); | |
| 173 | Array.Clear(k, 0, k.Length); | |
| 174 | return simple; | |
| 175 | } | |
| 176 | ||
| 177 | byte[] s1 = DumbMixA(k); | |
| 178 | int dynamicSeed = (int)(DateTime.UtcNow.Ticks ^ Application.identifier.GetHashCode() ^ Environment.TickCount); | |
| 179 | byte[] s2 = DumbMixB(s1, dynamicSeed); | |
| 180 | byte[] s3 = DumbMixC(s2, sessionNonce); | |
| 181 | for (int r = 0; r < 2; r++) | |
| 182 | { | |
| 183 | byte[] t = DumbMixA(s3); | |
| 184 | byte[] u = DumbMixB(t, dynamicSeed ^ r); | |
| 185 | for (int i = 0; i < k.Length; i++) s3[i] = (byte)(s3[i] ^ u[i % u.Length]); | |
| 186 | Array.Clear(t, 0, t.Length); | |
| 187 | Array.Clear(u, 0, u.Length); | |
| 188 | } | |
| 189 | Array.Clear(s1, 0, s1.Length); | |
| 190 | Array.Clear(s2, 0, s2.Length); | |
| 191 | byte[] result = new byte[k.Length]; | |
| 192 | for (int i = 0; i < k.Length; i++) result[i] = (byte)(k[i] ^ s3[i % s3.Length]); | |
| 193 | Array.Clear(k, 0, k.Length); | |
| 194 | Array.Clear(s3, 0, s3.Length); | |
| 195 | return result; | |
| 196 | } | |
| 197 | ||
| 198 | private (byte[] aesKey, byte[] hmacKey) DeriveKeysRotating(byte[] salt, byte[] rotationNonce) | |
| 199 | { | |
| 200 | byte[] secret = GetSecretBytes(); | |
| 201 | string secretStr = EnableHardening ? Convert.ToBase64String(secret) + Application.identifier : Encoding.UTF8.GetString(secret) + Application.identifier; | |
| 202 | Array.Clear(secret, 0, secret.Length); | |
| 203 | ||
| 204 | using (var kdf = new Rfc2898DeriveBytes(secretStr, salt, Pbkdf2Iterations, HashAlgorithmName.SHA256)) | |
| 205 | { | |
| 206 | byte[] km = kdf.GetBytes(KeyMaterialLen); | |
| 207 | if (EnableHardening && EnableRotation && rotationNonce != null) | |
| 208 | { | |
| 209 | for (int i = 0; i < km.Length; i++) km[i] ^= rotationNonce[i % rotationNonce.Length]; | |
| 210 | } | |
| 211 | byte[] aesKey = new byte[AesKeyLen]; | |
| 212 | byte[] hmacKey = new byte[HmacKeyLen]; | |
| 213 | Buffer.BlockCopy(km, 0, aesKey, 0, AesKeyLen); | |
| 214 | Buffer.BlockCopy(km, AesKeyLen, hmacKey, 0, HmacKeyLen); | |
| 215 | Array.Clear(km, 0, km.Length); | |
| 216 | return (aesKey, hmacKey); | |
| 217 | } | |
| 218 | } | |
| 219 | ||
| 220 | public void Save<T>(T data) | |
| 221 | { | |
| 222 | if (EnableHardening && (EnableAntiDebugChecks || EnableRootDetection) && IsTamperedOrDebug()) return; | |
| 223 | try | |
| 224 | { | |
| 225 | string json = JsonUtility.ToJson(data); | |
| 226 | byte[] salt = GenerateRandomBytes(SaltLenDefault); | |
| 227 | byte[] rotation = (EnableHardening && EnableRotation) ? GenerateRandomBytes(16) : new byte[16]; | |
| 228 | var keys = DeriveKeysRotating(salt, (EnableRotation ? rotation : null)); | |
| 229 | byte[] aesKey = keys.aesKey; | |
| 230 | byte[] hmacKey = keys.hmacKey; | |
| 231 | byte[] plain = Encoding.UTF8.GetBytes(json); | |
| 232 | byte[] iv; | |
| 233 | byte[] cipher; | |
| 234 | using (Aes aes = Aes.Create()) | |
| 235 | { | |
| 236 | aes.KeySize = 256; | |
| 237 | aes.Mode = CipherMode.CBC; | |
| 238 | aes.Padding = PaddingMode.PKCS7; | |
| 239 | aes.Key = aesKey; | |
| 240 | aes.GenerateIV(); | |
| 241 | iv = aes.IV; | |
| 242 | using (var enc = aes.CreateEncryptor(aes.Key, iv)) | |
| 243 | cipher = enc.TransformFinalBlock(plain, 0, plain.Length); | |
| 244 | } | |
| 245 | byte[] ivCipher = new byte[iv.Length + cipher.Length]; | |
| 246 | Buffer.BlockCopy(iv, 0, ivCipher, 0, iv.Length); | |
| 247 | Buffer.BlockCopy(cipher, 0, ivCipher, iv.Length, cipher.Length); | |
| 248 | byte[] hmac; | |
| 249 | using (var h = new HMACSHA256(hmacKey)) hmac = h.ComputeHash(ivCipher); | |
| 250 | using (MemoryStream ms = new MemoryStream()) | |
| 251 | { | |
| 252 | ms.Write(Encoding.ASCII.GetBytes(Magic), 0, 4); | |
| 253 | ms.WriteByte(Version); | |
| 254 | ms.WriteByte((byte)salt.Length); | |
| 255 | ms.Write(salt, 0, salt.Length); | |
| 256 | ms.WriteByte(EnableRotation ? (byte)rotation.Length : (byte)0); | |
| 257 | if (EnableRotation) ms.Write(rotation, 0, rotation.Length); | |
| 258 | ms.Write(iv, 0, iv.Length); | |
| 259 | byte[] cipherLenBytes = BitConverter.GetBytes((Int32)cipher.Length); | |
| 260 | if (!BitConverter.IsLittleEndian) Array.Reverse(cipherLenBytes); | |
| 261 | ms.Write(cipherLenBytes, 0, 4); | |
| 262 | ms.Write(cipher, 0, cipher.Length); | |
| 263 | ms.Write(hmac, 0, hmac.Length); | |
| 264 | string outBlob = Convert.ToBase64String(ms.ToArray()); | |
| 265 | File.WriteAllText(filePath, outBlob); | |
| 266 | } | |
| 267 | ClearBytes(aesKey); | |
| 268 | ClearBytes(hmacKey); | |
| 269 | ClearBytes(plain); | |
| 270 | ClearBytes(iv); | |
| 271 | ClearBytes(cipher); | |
| 272 | ClearBytes(ivCipher); | |
| 273 | ClearBytes(hmac); | |
| 274 | ClearBytes(salt); | |
| 275 | ClearBytes(rotation); | |
| 276 | } | |
| 277 | catch { } | |
| 278 | } | |
| 279 | ||
| 280 | public T Load<T>() | |
| 281 | { | |
| 282 | if (EnableHardening && (EnableAntiDebugChecks || EnableRootDetection) && IsTamperedOrDebug()) return default; | |
| 283 | if (!File.Exists(filePath)) return default; | |
| 284 | string blob = File.ReadAllText(filePath); | |
| 285 | byte[] total; | |
| 286 | try { total = Convert.FromBase64String(blob); } catch { return default; } | |
| 287 | try | |
| 288 | { | |
| 289 | using (MemoryStream ms = new MemoryStream(total)) | |
| 290 | using (BinaryReader br = new BinaryReader(ms)) | |
| 291 | { | |
| 292 | byte[] magic = br.ReadBytes(4); | |
| 293 | if (Encoding.ASCII.GetString(magic) != Magic) return default; | |
| 294 | byte ver = br.ReadByte(); | |
| 295 | if (ver != Version) return default; | |
| 296 | int saltLen = br.ReadByte(); | |
| 297 | if (saltLen <= 0 || saltLen > 64) return default; | |
| 298 | byte[] salt = br.ReadBytes(saltLen); | |
| 299 | int rotationLen = br.ReadByte(); | |
| 300 | byte[] rotation = rotationLen > 0 ? br.ReadBytes(rotationLen) : new byte[16]; | |
| 301 | byte[] iv = br.ReadBytes(IvLen); | |
| 302 | int cipherLen = br.ReadInt32(); | |
| 303 | if (!BitConverter.IsLittleEndian) cipherLen = System.Net.IPAddress.NetworkToHostOrder(cipherLen); | |
| 304 | if (cipherLen <= 0 || cipherLen > total.Length) return default; | |
| 305 | byte[] cipher = br.ReadBytes(cipherLen); | |
| 306 | byte[] hmac = br.ReadBytes(HmacLen); | |
| 307 | byte[] ivCipher = new byte[iv.Length + cipher.Length]; | |
| 308 | Buffer.BlockCopy(iv, 0, ivCipher, 0, iv.Length); | |
| 309 | Buffer.BlockCopy(cipher, 0, ivCipher, iv.Length, cipher.Length); | |
| 310 | var keys = DeriveKeysRotating(salt, (EnableRotation ? rotation : null)); | |
| 311 | byte[] aesKey = keys.aesKey; | |
| 312 | byte[] hmacKey = keys.hmacKey; | |
| 313 | byte[] expected; | |
| 314 | using (var h = new HMACSHA256(hmacKey)) expected = h.ComputeHash(ivCipher); | |
| 315 | bool ok = CryptographicOperations.FixedTimeEquals(expected, hmac); | |
| 316 | if (!ok) | |
| 317 | { | |
| 318 | ClearBytes(aesKey); | |
| 319 | ClearBytes(hmacKey); | |
| 320 | ClearBytes(expected); | |
| 321 | return default; | |
| 322 | } | |
| 323 | byte[] plain; | |
| 324 | using (Aes aes = Aes.Create()) | |
| 325 | { | |
| 326 | aes.KeySize = 256; | |
| 327 | aes.Mode = CipherMode.CBC; | |
| 328 | aes.Padding = PaddingMode.PKCS7; | |
| 329 | aes.Key = aesKey; | |
| 330 | aes.IV = iv; | |
| 331 | using (var dec = aes.CreateDecryptor(aes.Key, aes.IV)) | |
| 332 | plain = dec.TransformFinalBlock(cipher, 0, cipher.Length); | |
| 333 | } | |
| 334 | string json = Encoding.UTF8.GetString(plain); | |
| 335 | ClearBytes(aesKey); | |
| 336 | ClearBytes(hmacKey); | |
| 337 | ClearBytes(expected); | |
| 338 | ClearBytes(plain); | |
| 339 | ClearBytes(iv); | |
| 340 | ClearBytes(cipher); | |
| 341 | ClearBytes(salt); | |
| 342 | ClearBytes(rotation); | |
| 343 | return JsonUtility.FromJson<T>(json); | |
| 344 | } | |
| 345 | } | |
| 346 | catch { return default; } | |
| 347 | } | |
| 348 | ||
| 349 | private static byte[] GenerateRandomBytes(int len) | |
| 350 | { | |
| 351 | byte[] b = new byte[len]; | |
| 352 | using (var rng = RandomNumberGenerator.Create()) rng.GetBytes(b); | |
| 353 | return b; | |
| 354 | } | |
| 355 | ||
| 356 | private static void ClearBytes(byte[] b) | |
| 357 | { | |
| 358 | if (b == null) return; | |
| 359 | Array.Clear(b, 0, b.Length); | |
| 360 | } | |
| 361 | ||
| 362 | private string ResolveFileName(string baseName) | |
| 363 | { | |
| 364 | if (!EnableHardening || !EnableFileNameObfuscation) return baseName + ".dat"; | |
| 365 | byte[] nameBytes = Encoding.UTF8.GetBytes(baseName + Application.identifier); | |
| 366 | using (var sha = SHA256.Create()) | |
| 367 | { | |
| 368 | byte[] hash = sha.ComputeHash(nameBytes); | |
| 369 | string b64 = Convert.ToBase64String(hash); | |
| 370 | string safe = b64.Replace('+', '-').Replace('/', '_').Replace('=', 'x'); | |
| 371 | ClearBytes(hash); | |
| 372 | return safe.Substring(0, Math.Min(28, safe.Length)) + ".dat"; | |
| 373 | } | |
| 374 | } | |
| 375 | ||
| 376 | private bool IsTamperedOrDebug() | |
| 377 | { | |
| 378 | if (!EnableHardening) return false; | |
| 379 | try | |
| 380 | { | |
| 381 | if (EnableAntiDebugChecks) | |
| 382 | { | |
| 383 | if (Debug.isDebugBuild) return true; | |
| 384 | if (Debugger.IsAttached) return true; | |
| 385 | } | |
| 386 | } | |
| 387 | catch { } | |
| 388 | try | |
| 389 | { | |
| 390 | if (EnableRootDetection) | |
| 391 | { | |
| 392 | string[] suspectPaths = new string[] { "/system/bin/su", "/system/xbin/su", "/sbin/su" }; | |
| 393 | foreach (var p in suspectPaths) if (File.Exists(p)) return true; | |
| 394 | } | |
| 395 | } | |
| 396 | catch { } | |
| 397 | return false; | |
| 398 | } | |
| 399 | } }}} |