Skip to content

Commit

Permalink
Add APK extraction support, and output-root option. Closes #31
Browse files Browse the repository at this point in the history
  • Loading branch information
Sam Byass committed Sep 17, 2021
1 parent dc046b9 commit 813b471
Show file tree
Hide file tree
Showing 4 changed files with 142 additions and 60 deletions.
45 changes: 25 additions & 20 deletions Cpp2IL.Core/Cpp2IlApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,34 +36,39 @@ public static int[] DetermineUnityVersion(string unityPlayerPath, string gameDat
//Globalgamemanagers
var globalgamemanagersPath = Path.Combine(gameDataPath, "globalgamemanagers");
var ggmBytes = File.ReadAllBytes(globalgamemanagersPath);
var verString = new StringBuilder();
var idx = 0x14;
version = GetVersionFromGlobalGameManagers(ggmBytes);
}

return version;
}

public static int[] GetVersionFromGlobalGameManagers(byte[] ggmBytes)
{
var verString = new StringBuilder();
var idx = 0x14;
while (ggmBytes[idx] != 0)
{
verString.Append(Convert.ToChar(ggmBytes[idx]));
idx++;
}

var unityVer = verString.ToString();

if (!unityVer.Contains("f"))
{
idx = 0x30;
verString = new StringBuilder();
while (ggmBytes[idx] != 0)
{
verString.Append(Convert.ToChar(ggmBytes[idx]));
idx++;
}

var unityVer = verString.ToString();

if (!unityVer.Contains("f"))
{
idx = 0x30;
verString = new StringBuilder();
while (ggmBytes[idx] != 0)
{
verString.Append(Convert.ToChar(ggmBytes[idx]));
idx++;
}

unityVer = verString.ToString();
}

unityVer = unityVer[..unityVer.IndexOf("f", StringComparison.Ordinal)];
version = unityVer.Split('.').Select(int.Parse).ToArray();
unityVer = verString.ToString();
}

return version;
unityVer = unityVer[..unityVer.IndexOf("f", StringComparison.Ordinal)];
return unityVer.Split('.').Select(int.Parse).ToArray();
}

private static void ConfigureLib(bool allowUserToInputAddresses)
Expand Down
4 changes: 4 additions & 0 deletions Cpp2IL/CommandLineArgs.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Diagnostics.CodeAnalysis;
using System.IO;
using CommandLine;

namespace Cpp2IL
Expand Down Expand Up @@ -49,6 +50,9 @@ public class CommandLineArgs
[Option("parallel", HelpText = "Run analysis in parallel. Might break things.")]
public bool Parallel { get; set; }

[Option("output-root", HelpText = "Root directory to output to. Defaults to cpp2il_out in the current working directory.")]
public string OutputRootDir { get; set; } = Path.GetFullPath("cpp2il_out");

internal bool AreForceOptionsValid
{
get
Expand Down
149 changes: 109 additions & 40 deletions Cpp2IL/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.IO.Compression;
using System.Linq;
using CommandLine;
using Cpp2IL.Core;
Expand All @@ -15,6 +16,8 @@ namespace Cpp2IL
[SuppressMessage("ReSharper", "ClassNeverInstantiated.Global")]
internal class Program
{
private static readonly List<string> _pathsToDeleteOnExit = new List<string>();

private static readonly string[] BlacklistedExecutableFilenames =
{
"UnityCrashHandler.exe",
Expand All @@ -24,52 +27,105 @@ internal class Program
"MelonLoader.Installer.exe"
};

public static Cpp2IlRuntimeArgs GetRuntimeOptionsFromCommandLine(string[] commandLine)
private static void ResolvePathsFromCommandLine(string gamePath, string? inputExeName, ref Cpp2IlRuntimeArgs args)
{
var parserResult = Parser.Default.ParseArguments<CommandLineArgs>(commandLine);
if (Directory.Exists(gamePath))
{
//Windows game.
args.PathToAssembly = Path.Combine(gamePath, "GameAssembly.dll");
var exeName = Path.GetFileNameWithoutExtension(Directory.GetFiles(gamePath)
.First(f => f.EndsWith(".exe") && !BlacklistedExecutableFilenames.Any(bl => f.EndsWith(bl))));

if(parserResult is NotParsed<CommandLineArgs> notParsed && notParsed.Errors.Count() == 1 && notParsed.Errors.All(e => e.Tag == ErrorType.VersionRequestedError || e.Tag == ErrorType.HelpRequestedError))
//Version or help requested
Environment.Exit(0);

if (!(parserResult is Parsed<CommandLineArgs> {Value: { } options}))
throw new SoftException("Failed to parse command line arguments");
exeName = inputExeName ?? exeName;

if (!options.AreForceOptionsValid)
throw new SoftException("Invalid force option configuration");
var unityPlayerPath = Path.Combine(gamePath, $"{exeName}.exe");
args.PathToMetadata = Path.Combine(gamePath, $"{exeName}_Data", "il2cpp_data", "Metadata", "global-metadata.dat");

var result = new Cpp2IlRuntimeArgs();
if (!File.Exists(args.PathToAssembly) || !File.Exists(unityPlayerPath) || !File.Exists(args.PathToMetadata))
throw new SoftException("Invalid game-path or exe-name specified. Failed to find one of the following:\n" +
$"\t{args.PathToAssembly}\n" +
$"\t{unityPlayerPath}\n" +
$"\t{args.PathToMetadata}\n");

if (options.ForcedBinaryPath == null)
args.UnityVersion = Cpp2IlApi.DetermineUnityVersion(unityPlayerPath, Path.Combine(gamePath, $"{exeName}_Data"));

Logger.InfoNewline($"Determined game's unity version to be {string.Join(".", args.UnityVersion)}");

if (args.UnityVersion[0] <= 4)
throw new SoftException($"Unable to determine a valid unity version (got {args.UnityVersion.ToStringEnumerable()})");

args.Valid = true;
}
else if (File.Exists(gamePath) && Path.GetExtension(gamePath).ToLowerInvariant() == ".apk")
{
var baseGamePath = options.GamePath;
//APK
//Metadata: assets/bin/Data/Managed/Metadata
//Binary: lib/(armeabi-v7a)|(arm64-v8a)/libil2cpp.so

Logger.InfoNewline($"Attempting to extract required files from APK {gamePath}");

if (!Directory.Exists(baseGamePath))
throw new SoftException($"Specified game-path does not exist: {baseGamePath}");
using var stream = File.OpenRead(gamePath);
using var zipArchive = new ZipArchive(stream);

result.PathToAssembly = Path.Combine(baseGamePath, "GameAssembly.dll");
var exeName = Path.GetFileNameWithoutExtension(Directory.GetFiles(baseGamePath)
.First(f => f.EndsWith(".exe") && !BlacklistedExecutableFilenames.Any(bl => f.EndsWith(bl))));
var globalMetadata = zipArchive.Entries.FirstOrDefault(e => e.FullName.EndsWith("assets/bin/Data/Managed/Metadata/global-metadata.dat"));
var binary = zipArchive.Entries.FirstOrDefault(e => e.FullName.EndsWith("lib/arm64-v8a/libil2cpp.so"));
binary ??= zipArchive.Entries.FirstOrDefault(e => e.FullName.EndsWith("lib/armeabi-v7a/libil2cpp.so"));

exeName = options.ExeName ?? exeName;
var globalgamemanagers = zipArchive.Entries.FirstOrDefault(e => e.FullName.EndsWith("assets/bin/Data/globalgamemanagers"));

var unityPlayerPath = Path.Combine(baseGamePath, $"{exeName}.exe");
result.PathToMetadata = Path.Combine(baseGamePath, $"{exeName}_Data", "il2cpp_data", "Metadata", "global-metadata.dat");
if (binary == null)
throw new SoftException("Could not find libil2cpp.so inside the apk.");
if (globalMetadata == null)
throw new SoftException("Could not find global-metadata.dat inside the apk");
if (globalgamemanagers == null)
throw new SoftException("Could not find globalgamemanagers inside the apk");

if (!File.Exists(result.PathToAssembly) || !File.Exists(unityPlayerPath) || !File.Exists(result.PathToMetadata))
throw new SoftException("Invalid game-path or exe-name specified. Failed to find one of the following:\n" +
$"\t{result.PathToAssembly}\n" +
$"\t{unityPlayerPath}\n" +
$"\t{result.PathToMetadata}\n");
var tempFileBinary = Path.GetTempFileName();
var tempFileMeta = Path.GetTempFileName();

result.UnityVersion = Cpp2IlApi.DetermineUnityVersion(unityPlayerPath, Path.Combine(baseGamePath, $"{exeName}_Data"));
_pathsToDeleteOnExit.Add(tempFileBinary);
_pathsToDeleteOnExit.Add(tempFileMeta);

Logger.InfoNewline($"Determined game's unity version to be {string.Join(".", result.UnityVersion)}");
Logger.InfoNewline($"Extracting APK/{binary.FullName} to {tempFileBinary}");
binary.ExtractToFile(tempFileBinary, true);
Logger.InfoNewline($"Extracting APK/{globalMetadata.FullName} to {tempFileMeta}");
globalMetadata.ExtractToFile(tempFileMeta, true);

if (result.UnityVersion[0] <= 4)
throw new SoftException($"Unable to determine a valid unity version (got {result.UnityVersion.ToStringEnumerable()})");
args.PathToAssembly = tempFileBinary;
args.PathToMetadata = tempFileMeta;

result.Valid = true;
Logger.InfoNewline("Reading globalgamemanagers to determine unity version...");
var ggmBytes = new byte[0x40];
using var ggmStream = globalgamemanagers.Open();
ggmStream.Read(ggmBytes, 0, 0x40);

args.UnityVersion = Cpp2IlApi.GetVersionFromGlobalGameManagers(ggmBytes);

Logger.InfoNewline($"Determined game's unity version to be {string.Join(".", args.UnityVersion)}");

args.Valid = true;
}
}

private static Cpp2IlRuntimeArgs GetRuntimeOptionsFromCommandLine(string[] commandLine)
{
var parserResult = Parser.Default.ParseArguments<CommandLineArgs>(commandLine);

if (parserResult is NotParsed<CommandLineArgs> notParsed && notParsed.Errors.Count() == 1 && notParsed.Errors.All(e => e.Tag == ErrorType.VersionRequestedError || e.Tag == ErrorType.HelpRequestedError))
//Version or help requested
Environment.Exit(0);

if (!(parserResult is Parsed<CommandLineArgs> { Value: { } options }))
throw new SoftException("Failed to parse command line arguments");

if (!options.AreForceOptionsValid)
throw new SoftException("Invalid force option configuration");

var result = new Cpp2IlRuntimeArgs();

if (options.ForcedBinaryPath == null)
{
ResolvePathsFromCommandLine(options.GamePath, options.ExeName, ref result);
}
else
{
Expand All @@ -94,9 +150,9 @@ public static Cpp2IlRuntimeArgs GetRuntimeOptionsFromCommandLine(string[] comman
Logger.WarnNewline("!!!!!!!!!!You have enabled IL-To-ASM. If this breaks, it breaks.!!!!!!!!!!");
}

result.AnalysisLevel = (AnalysisLevel) options.AnalysisLevel;
result.OutputRootDirectory = Path.GetFullPath("cpp2il_out");
result.AnalysisLevel = (AnalysisLevel)options.AnalysisLevel;

result.OutputRootDirectory = options.OutputRootDir;

return result;
}
Expand All @@ -107,7 +163,7 @@ public static int Main(string[] args)
Console.WriteLine("A Tool to Reverse Unity's \"il2cpp\" Build Process.\n");

ConsoleLogger.Initialize();

Logger.InfoNewline("Running on " + Environment.OSVersion.Platform);

try
Expand Down Expand Up @@ -171,14 +227,27 @@ public static int MainWithArgs(Cpp2IlRuntimeArgs runtimeArgs)

Logger.InfoNewline($"Finished Applying Attributes in {(DateTime.Now - start).TotalMilliseconds:F0}ms");

if (runtimeArgs.EnableAnalysis)
if (runtimeArgs.EnableAnalysis)
Cpp2IlApi.PopulateConcreteImplementations();

Cpp2IlApi.SaveAssemblies(runtimeArgs.OutputRootDirectory);

if (runtimeArgs.EnableAnalysis)
if (runtimeArgs.EnableAnalysis)
DoAssemblyCSharpAnalysis(runtimeArgs.AssemblyToRunAnalysisFor, runtimeArgs.AnalysisLevel, runtimeArgs.OutputRootDirectory, keyFunctionAddresses!, runtimeArgs.EnableIlToAsm, runtimeArgs.Parallel);

foreach (var p in _pathsToDeleteOnExit)
{
try
{
Logger.InfoNewline($"Cleaning up {p}...");
File.Delete(p);
}
catch (Exception)
{
//Ignore
}
}

Logger.InfoNewline("Done.");
return 0;
}
Expand All @@ -191,9 +260,9 @@ private static void DoAssemblyCSharpAnalysis(string assemblyName, AnalysisLevel
return;

Cpp2IlApi.AnalyseAssembly(analysisLevel, assemblyCsharp, keyFunctionAddresses, Path.Combine(rootDir, "types"), parallel);
if(doIlToAsm)
Cpp2IlApi.SaveAssemblies(rootDir, new List<AssemblyDefinition> {assemblyCsharp});

if (doIlToAsm)
Cpp2IlApi.SaveAssemblies(rootDir, new List<AssemblyDefinition> { assemblyCsharp });
}
}
}
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ The link above will take you to the documentation for LibCpp2IL.
The simplest usage of this application is for a windows x86 or x64 unity game. In that case you can just run `Cpp2IL-Win.exe --game-path=C:\Path\To\Your\Game`
and Cpp2IL will detect your unity version, locate the files it needs, and dump the output into a cpp2il_out folder wherever you ran the command from.

Assuming you have a single APK file (not an APKM or XAPK), and are running at least cpp2il 2021.4.0 (or the pre-release for it), you
can use the same argument as above but pass in the path to the APK, and cpp2il will extract the files it needs from the APK.

### Supported Command Line Option Listing

| Option | Argument Example | Description |
Expand All @@ -34,6 +37,7 @@ and Cpp2IL will detect your unity version, locate the files it needs, and dump t
| --suppress-attributes | &lt;None> | Prevents generated DLLs from containing attributes providing il2cpp-specific metadata, such as function pointers, etc. |
| --parallel | &lt;None> | Run analysis in parallel. Usually much faster, but may be unstable. Also puts your CPU under a lot of strain (100% usage is targeted). |
| --run-analysis-for-assembly | mscorlib | Run analysis for the specified assembly. Do not specify the `.dll` extension. |
| --output-root | cpp2il_out | Root directory to output to. Dummy DLLs will be put directly in here, and analysis results will be put in a types folder inside. |

## Release Structure

Expand Down

0 comments on commit 813b471

Please sign in to comment.