-
Notifications
You must be signed in to change notification settings - Fork 2
/
Plugin.cs
389 lines (333 loc) · 17.8 KB
/
Plugin.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Text.RegularExpressions;
using Il2CppInspector;
using Il2CppInspector.PluginAPI;
using Il2CppInspector.PluginAPI.V100;
using Il2CppInspector.Reflection;
using NoisyCowStudios.Bin2Object;
using static Loader.Utils;
using Assembly = System.Reflection.Assembly;
namespace Loader
{
/// <summary>
/// Il2CppInspector plugin to enable loading of Girls' Frontline (少女前线).
///
/// <para />
/// If you want to follow along, offsets in this project refer to <c>arm64-v8a</c> binary <c>libtprt.so</c>
/// from EN client v<c>2.0702_362</c>, app id <c>com.sunborn.girlsfrontline.en</c>.
///
/// <para />
/// See <see cref="Utils">Utils.cs</see> for the actual decryption methods.
/// </summary>
public class Plugin : IPlugin, ILoadPipeline
{
public string Id => "girlsfrontline-deobfuscator";
public string Name => "Girls' Frontline Deobfuscator";
public string Author => "neko-gg";
public string Version => "1.1";
public string Description => "Enables loading of Girls' Frontline (少女前线)";
private string PreferredArch { get; set; } = null;
private readonly PluginOptionBoolean _stcExportEnabledOption = new PluginOptionBoolean
{
Name = "stc-export-enabled",
Description = "Export STC format files",
Value = true,
Required = true
};
private readonly PluginOptionFilePath _stcFormatPathOption = new PluginOptionFilePath
{
Name = "stc-format-path",
Description = "Output folder for STC format files\n⚠️This directory and all its content will be deleted!",
IsFolder = true,
MustExist = false,
Value = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "girls-frontline", "stc-format"),
Required = true
};
private readonly PluginOptionNumber<uint> _headerKeySeed0Option = new PluginOptionNumber<uint>
{
Name = "header-key-seed-0",
Description = "Metadata header decryption key seed [0]",
Value = 0xDCD8DB8F, // offset: 0x14DEE9
Required = true,
If = () => true,
Style = PluginOptionNumberStyle.Hex
};
private readonly PluginOptionNumber<uint> _headerKeySeed1Option = new PluginOptionNumber<uint>
{
Name = "header-key-seed-1",
Description = "Metadata header decryption key seed [1]",
Value = 0x8EDCDF8C, // offset: 0x14DEED
Required = true,
If = () => true,
Style = PluginOptionNumberStyle.Hex
};
private readonly PluginOptionNumber<uint> _headerKeySeed2Option = new PluginOptionNumber<uint>
{
Name = "header-key-seed-2",
Description = "Metadata header decryption key seed [2]",
Value = 0x8BD8DB8F, // offset: 0x14DEF1
Required = true,
If = () => true,
Style = PluginOptionNumberStyle.Hex
};
private readonly PluginOptionNumber<uint> _headerKeySeed3Option = new PluginOptionNumber<uint>
{
Name = "header-key-seed-3",
Description = "Metadata header decryption key seed [3]",
Value = 0x8A8A8E89, // offset: 0x14DEF5
Required = true,
If = () => true,
Style = PluginOptionNumberStyle.Hex
};
private readonly PluginOptionNumber<byte> _headerLastPassKeySeedOption = new PluginOptionNumber<byte>
{
Name = "header-last-pass-key-seed",
Description = "Metadata header last pass key seed",
Value = 0x02, // offset: 0xD33E4 - 0xD341C
Required = true,
If = () => true,
Style = PluginOptionNumberStyle.Hex
};
private readonly PluginOptionNumber<byte> _headerLastBytesKeyOption = new PluginOptionNumber<byte>
{
Name = "header-last-bytes-key",
Description = "Metadata header last bytes decryption key",
Value = 0xAF, // offset: 0xC5BD4
Required = true,
If = () => true,
Style = PluginOptionNumberStyle.Hex
};
private readonly PluginOptionNumber<byte> _bodyKeySeedOption = new PluginOptionNumber<byte>
{
Name = "body-key-seed",
Description = "Metadata body decryption key seed",
Value = 0xBF, // offset: 0x1CF20
Required = true,
If = () => true,
Style = PluginOptionNumberStyle.Hex
};
private readonly PluginOptionNumber<ushort> _binaryXorStripeSizeOption = new PluginOptionNumber<ushort>
{
Name = "binary-xor-stripe-size",
Description = "IL2CPP binary image XOR stripe size",
Value = 0x1000, // no offsets here, I eyeballed it
Required = true,
If = () => true,
Style = PluginOptionNumberStyle.Hex
};
public List<IPluginOption> Options => new List<IPluginOption>
{
_stcExportEnabledOption,
_stcFormatPathOption,
_headerKeySeed0Option,
_headerKeySeed1Option,
_headerKeySeed2Option,
_headerKeySeed3Option,
_headerLastPassKeySeedOption,
_headerLastBytesKeyOption,
_bodyKeySeedOption,
_binaryXorStripeSizeOption
};
public Plugin()
{
_stcFormatPathOption.If = () => _stcExportEnabledOption.Value;
}
public void PreProcessMetadata(BinaryObjectStream stream, PluginPreProcessMetadataEventInfo info)
{
PluginServices.For(this).StatusUpdate("Decrypting metadata");
byte[] metadata = stream.ToArray();
// we start our metadata header decryption journey with key 0:
// from there, we will calculate key 1, key 2, and finally key 3
// key 0 -> key 1 -> key 2 -> key 3
// key 0 is hardcoded in the binary at 0x14DEE9
PluginOptionNumber<uint>[] headerKeySeeds = {_headerKeySeed0Option, _headerKeySeed1Option, _headerKeySeed2Option, _headerKeySeed3Option};
byte[] key0 = headerKeySeeds.Select(option => option.Value).SelectMany(DWordToByteArray).ToArray();
Debug.WriteLine("key 0:");
PrintBuffer(key0);
// key 1 is derived from key 0
byte[] key1 = GetKey1(metadata, key0);
Debug.WriteLine("key 1:");
PrintBuffer(key1);
// key 2 is derived from key 1
uint[] key2 = GetKey2(key1);
Debug.WriteLine("key 2:");
PrintBuffer(key2);
// key 3 is derived from key 2
// this is the key used to decrypt the bulk of metadata header, bytes 0x8 to 0x108
uint[] key3 = GetKey3(key2);
Debug.WriteLine("key 3:");
PrintBuffer(key3);
// last pass header key is calculated in the binary at around 0xD33E4 - 0xD341C;
// it's a fuck fest of vector operations, but the result is pretty banal:
// 0x02, 0x03, ..., 0x41
// the first 16 bytes of this key are used at the very end of bytes 0x8 to 0x108 decryption process,
// hence the name
byte[] lastPassHeaderKey = GetLastPassHeaderKey(_headerLastPassKeySeedOption.Value);
Debug.WriteLine("last pass header key:");
PrintBuffer(lastPassHeaderKey);
// the first 8 bytes are hardcoded in the binary at around 0x9A9A0 - 0x9AA00 to:
// 0x0 0x0 0x0 0x0 [whatever it was in the encrypted metadata] 0x00 0x00 0x00;
// the last 8 bytes, 0x108 to 0x10F, are XOR-encrypted with a single byte,
// and this byte is hardcoded in a MOV operation in the binary at 0xC5BD4;
// with key 3, last pass header key, and this last bytes key, we can now decrypt the whole metadata header
byte[] decryptedMetadataHeader = DecryptMetadataHeader(metadata, key3, lastPassHeaderKey, _headerLastBytesKeyOption.Value);
Debug.WriteLine("decrypted metadata header:");
PrintBuffer(decryptedMetadataHeader);
// metadata body (everything after the header) is XOR-encrypted;
// the decryption key is generated from an initial hardcoded value:
// the return value of the subroutine starting at 0x1CF20;
// full decryption key is then calculated in the subroutine at 0xC5C20
byte[] decryptedMetadataBody = DecryptMetadataBody(metadata, _bodyKeySeedOption.Value);
Debug.WriteLine("decrypted metadata body (first 256 bytes):");
PrintBuffer(decryptedMetadataBody.Take(0x100).ToArray());
// decrypted metadata is the concatenation of decrypted header and decrypted body
stream.Write(0, decryptedMetadataHeader.Concat(decryptedMetadataBody).ToArray());
info.IsStreamModified = true;
}
public void PostProcessImage<T>(FileFormatStream<T> stream, PluginPostProcessImageEventInfo info) where T : FileFormatStream<T>
{
if (!(stream is ElfReader32 || stream is ElfReader64))
{
Debug.WriteLine($"stream is neither ElfReader32 nor ElfReader64, but {stream.GetType()}; skipping");
return;
}
if (String.IsNullOrEmpty(PreferredArch) || stream.Arch == "ARM64") PreferredArch = stream.Arch;
PluginServices.For(this).StatusUpdate($"Decrypting {stream.Arch} IL2CPP binary image");
Dictionary<string, Section> sections = stream.GetSections().GroupBy(s => s.Name).ToDictionary(s => s.Key, s => s.First());
if (!sections.ContainsKey(".rodata") || !sections.ContainsKey(".text"))
{
Debug.WriteLine($"no .rodata or .text section found in {stream.Arch} IL2CPP binary image");
return;
}
// .rodata and .text sections of IL2CPP binary are XOR-encrypted in stripes with a single-byte key;
// we use a very crude method to determine which one: assume the most common byte in the first stripes
// of .rodata is 0x00; this is usually the case, representing ~50% of all bytes.
Section roDataSection = sections[".rodata"];
Section textSection = sections[".text"];
// even though only odd stripes are encrypted, we also try and decrypt even ones because
// Il2CppInspector XOR-Decryptor plugin likes to sometimes assume that the assembly it's not actually
// striped, so we reverse the "encryption" if that's the case;
// thanks to XOR properties, if there's nothing to decrypt, the most common byte would be 0x00
// and we'd basically end up doing nothing (A ^ 0 == A, for every A), so no extra checks are performed
int stripeSize = _binaryXorStripeSizeOption.Value;
int firstBlockLength = GetFirstBlockLength(roDataSection, stripeSize);
byte oddMostCommonByte = MostCommonByte(stream, roDataSection.ImageStart, 0, firstBlockLength);
byte evenMostCommonByte = MostCommonByte(stream, roDataSection.ImageStart, firstBlockLength, stripeSize);
XorSection(stream, textSection, stripeSize, firstBlockLength, oddMostCommonByte, evenMostCommonByte);
XorSection(stream, roDataSection, stripeSize, firstBlockLength, oddMostCommonByte, evenMostCommonByte);
info.IsStreamModified = true;
}
public void PostProcessTypeModel(TypeModel model, PluginPostProcessTypeModelEventInfo data)
{
if (!_stcExportEnabledOption.Value)
{
Debug.WriteLine("STC format files export is disabled; skipping");
return;
}
if (PreferredArch != model.Package.BinaryImage.Arch)
{
Debug.WriteLine($"skipping STC format files export for arch {model.Package.BinaryImage.Arch}");
return;
}
PluginServices.For(this).StatusUpdate("Exporting STC format files");
if (Directory.Exists(_stcFormatPathOption.Value))
{
Debug.WriteLine($"recursively deleting directory ${_stcFormatPathOption.Value}");
Directory.Delete(_stcFormatPathOption.Value, true);
}
Dictionary<int, StcFormat> stcFormatDictionary = model.TypesByFullName["Cmd.CmdDef"]
.DeclaredFields
.Where(field => field.FieldType.IsEnum)
.ToDictionary(field => (int) field.DefaultValue, field => GetStcFormat(model, field));
ExportGflDataMinerStcFormatFiles(stcFormatDictionary);
ExportGfDecompressStcFormatFiles(stcFormatDictionary);
}
private static byte MostCommonByte(IFileFormatStream stream, long imageStart, long offset, int count)
{
byte[] bytes = stream.ReadBytes(imageStart + offset, count);
KeyValuePair<byte, int> mostCommonByteWithCount = MostCommonByteWithCount(bytes);
byte mostCommonByte = mostCommonByteWithCount.Key;
int mostCommonByteCount = mostCommonByteWithCount.Value;
Debug.WriteLine($"[{stream.Arch}] most common byte in {(offset == 0 ? "first" : "second")} stripe of .rodata is 0x{mostCommonByte:X2} with {mostCommonByteCount} occurrences in {bytes.Length} bytes ({Math.Round((double) mostCommonByteCount / bytes.Length * 100d)}%)");
return mostCommonByte;
}
private static void XorSection(BinaryObjectStream stream, Section section, int stripeSize, int firstBlockLength, byte oddXorValue, byte evenXorValue)
{
long start = section.ImageStart;
int length = section.ImageLength;
XorStripe(stream, start, firstBlockLength, oddXorValue);
bool oddStripe = false;
for (long position = start + firstBlockLength; position < start + length; position += stripeSize)
{
int size = (int) Math.Min(stripeSize, start + length - position);
XorStripe(stream, position, size, oddStripe ? oddXorValue : evenXorValue);
oddStripe = !oddStripe;
}
}
private static void XorStripe(BinaryObjectStream stream, long offset, int length, byte xorKey)
{
byte[] bytes = stream.ReadBytes(offset, length);
bytes = XorBytes(bytes, xorKey);
stream.Write(offset, bytes);
}
private static int GetFirstBlockLength(Section section, int stripeSize)
{
long start = (int) section.ImageStart;
long firstBlockLength = stripeSize;
long extraCount = start % stripeSize;
if (extraCount != 0)
firstBlockLength += stripeSize - extraCount;
return (int) firstBlockLength;
}
private StcFormat GetStcFormat(TypeModel model, FieldInfo fieldInfo)
{
string name = new Regex("^stc(.*)List$").Replace(fieldInfo.Name, "$1").ToLowerInvariant();
return new StcFormat
{
Name = name,
Fields = model.TypesByFullName[$"Cmd.Stc{name[0].ToString().ToUpperInvariant()}{name.Substring(1)}"]
.DeclaredFields
.Where(field => field.IsPublic)
.Select(field => field.Name)
.ToList()
};
}
private void ExportGflDataMinerStcFormatFiles(Dictionary<int, StcFormat> stcFormatDictionary)
{
JsonSerializerOptions jsonSerializerOptions = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = true
};
foreach (var stcFormatEntry in stcFormatDictionary)
{
FileInfo file = new FileInfo(Path.Combine(_stcFormatPathOption.Value, "gfl-data-miner", $"{stcFormatEntry.Key}.json"));
Debug.Assert(file.Directory != null, "stc file directory cannot be null");
file.Directory.Create();
string jsonData = JsonSerializer.Serialize(stcFormatEntry.Value, jsonSerializerOptions);
// very crude way to pretty print STC format files with 4 spaces instead of 2
string formattedJsonData = new[] {4, 2}.Select(n => new string(' ', n))
.Aggregate(jsonData, (acc, s) => new Regex($"^{s}(?! )(.*)", RegexOptions.Multiline).Replace(acc, $"{s}{s}$1"));
File.WriteAllText(file.FullName, formattedJsonData);
}
}
private void ExportGfDecompressStcFormatFiles(Dictionary<int, StcFormat> stcFormatDictionary)
{
foreach (var stcFormatEntry in stcFormatDictionary)
{
FileInfo file = new FileInfo(Path.Combine(_stcFormatPathOption.Value, "GFDecompress", $"{stcFormatEntry.Key}.format"));
Debug.Assert(file.Directory != null, "stc file directory cannot be null");
file.Directory.Create();
string formatData = String.Join(Environment.NewLine, stcFormatEntry.Value.Fields);
File.WriteAllText(file.FullName, formatData);
}
string gfDecompressMapping = String.Join($",{Environment.NewLine}", stcFormatDictionary.Select(e => $"{{ \"{e.Key}.stc\", \"{e.Value.Name}\" }}"));
File.WriteAllText(Path.Combine(_stcFormatPathOption.Value, "GFDecompress", "mapping.txt"), gfDecompressMapping);
}
}
}