Support extracting INI binaries embedded in the kernel

This commit is contained in:
Alex Barney 2020-08-10 20:07:52 -07:00
parent 8491ec2117
commit 6bab1d9273
9 changed files with 196 additions and 17 deletions

View file

@ -387,6 +387,8 @@ Module,DescriptionStart,DescriptionEnd,Name,Summary
428,51,,ServiceNotInitialized,
428,101,,NotImplemented,
428,1000,1999,InvalidData,
428,1001,1019,InvalidInitialProcessData,
428,1002,1009,InvalidKip,

Can't render this file because it has a wrong number of fields in line 153.

View file

@ -5,6 +5,7 @@ using LibHac.Common;
using LibHac.Crypto;
using LibHac.Fs;
using LibHac.FsSystem;
using LibHac.Kernel;
namespace LibHac.Boot
{
@ -13,6 +14,9 @@ namespace LibHac.Boot
/// </summary>
public class Package2StorageReader
{
private const int KernelPayloadIndex = 0;
private const int IniPayloadIndex = 1;
private IStorage _storage;
private Package2Header _header;
private Keyset _keyset;
@ -69,6 +73,47 @@ namespace LibHac.Boot
return Result.Success;
}
/// <summary>
/// Opens an <see cref="IStorage"/> of the kernel payload.
/// </summary>
/// <param name="kernelStorage">If the method returns successfully, contains an <see cref="IStorage"/>
/// of the kernel payload.</param>
/// <returns>The <see cref="Result"/> of the operation.</returns>
public Result OpenKernel(out IStorage kernelStorage)
{
return OpenPayload(out kernelStorage, KernelPayloadIndex);
}
/// <summary>
/// Opens an <see cref="IStorage"/> of the initial process binary. If the binary is embedded in
/// the kernel, this method will attempt to locate and return the embedded binary.
/// </summary>
/// <param name="iniStorage">If the method returns successfully, contains an <see cref="IStorage"/>
/// of the initial process binary.</param>
/// <returns>The <see cref="Result"/> of the operation.</returns>
public Result OpenIni(out IStorage iniStorage)
{
if (HasIniPayload())
{
return OpenPayload(out iniStorage, IniPayloadIndex);
}
// Ini is embedded in the kernel
iniStorage = default;
Result rc = OpenKernel(out IStorage kernelStorage);
if (rc.IsFailure()) return rc;
if (!IniExtract.TryGetIni1Offset(out int offset, out int size, kernelStorage))
{
// Unable to find the ini. Could be a new, unsupported layout.
return ResultLibHac.NotImplemented.Log();
}
iniStorage = new SubStorage(kernelStorage, offset, size);
return Result.Success;
}
/// <summary>
/// Verifies the signature, metadata and payloads in the package.
/// </summary>
@ -216,5 +261,10 @@ namespace LibHac.Boot
// Copy the IV to the output because the IV field will be garbage after "decrypting" it
Unsafe.As<Package2Meta, Buffer16>(ref dest) = iv;
}
private bool HasIniPayload()
{
return _header.Meta.PayloadSizes[IniPayloadIndex] != 0;
}
}
}

View file

@ -28,6 +28,8 @@ namespace LibHac.Common
/// <summary>Error code: 2428-0051; Inner value: 0x67ac</summary>
public static Result.Base ServiceNotInitialized => new Result.Base(ModuleLibHac, 51);
/// <summary>Error code: 2428-0101; Inner value: 0xcbac</summary>
public static Result.Base NotImplemented => new Result.Base(ModuleLibHac, 101);
/// <summary>Error code: 2428-1000; Range: 1000-1999; Inner value: 0x7d1ac</summary>
public static Result.Base InvalidData { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => new Result.Base(ModuleLibHac, 1000, 1999); }

View file

@ -0,0 +1,100 @@
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using LibHac.Common;
using LibHac.Fs;
namespace LibHac.Kernel
{
public static class IniExtract
{
/// <summary>
/// Locates and returns the offset and size of the initial process binary embedded in the kernel.
/// The INI is only embedded in the kernel in system versions >= 8.0.0.
/// </summary>
/// <param name="offset">When this method returns, contains the offset of
/// the INI inside the kernel if it was found.</param>
/// <param name="size">When this method returns, contains the size of the INI if it was found.</param>
/// <param name="kernelStorage">An <see cref="IStorage"/> containing the kernel to search.</param>
/// <returns><see langword="true"/> if the embedded INI was found.</returns>
public static bool TryGetIni1Offset(out int offset, out int size, IStorage kernelStorage)
{
offset = 0;
size = 0;
if (kernelStorage.GetSize(out long kernelSizeLong).IsFailure())
return false;
uint kernelSize = (uint)kernelSizeLong;
using (var array = new RentedArray<byte>(0x1000 + Unsafe.SizeOf<KernelMap>()))
{
// The kernel map should be in the first 0x1000 bytes
if (kernelStorage.Read(0, array.Span).IsFailure())
return false;
ref byte start = ref array.Span[0];
// Search every 4 bytes for a valid kernel map
for (int i = 0; i < 0x1000; i += sizeof(int))
{
ref KernelMap map = ref Unsafe.As<byte, KernelMap>(ref Unsafe.Add(ref start, i));
if (IsValidKernelMap(in map, kernelSize))
{
// Verify the ini header at the offset in the found map
var header = new InitialProcessBinaryReader.IniHeader();
if (kernelStorage.Read(map.Ini1StartOffset, SpanHelpers.AsByteSpan(ref header)).IsFailure())
return false;
if (header.Magic != InitialProcessBinaryReader.ExpectedMagic)
return false;
offset = (int)map.Ini1StartOffset;
size = header.Size;
return true;
}
}
return false;
}
}
private static bool IsValidKernelMap(in KernelMap map, uint maxSize)
{
if (map.TextStartOffset != 0) return false;
if (map.TextStartOffset >= map.TextEndOffset) return false;
if ((map.TextEndOffset & 0xFFF) != 0) return false;
if (map.TextEndOffset > map.RodataStartOffset) return false;
if ((map.RodataStartOffset & 0xFFF) != 0) return false;
if (map.RodataStartOffset >= map.RodataEndOffset) return false;
if ((map.RodataEndOffset & 0xFFF) != 0) return false;
if (map.RodataEndOffset > map.DataStartOffset) return false;
if ((map.DataStartOffset & 0xFFF) != 0) return false;
if (map.DataStartOffset >= map.DataEndOffset) return false;
if (map.DataEndOffset > map.BssStartOffset) return false;
if (map.BssStartOffset > map.BssEndOffset) return false;
if (map.BssEndOffset > map.Ini1StartOffset) return false;
if (map.Ini1StartOffset > maxSize - Unsafe.SizeOf<KernelMap>()) return false;
return true;
}
[StructLayout(LayoutKind.Sequential)]
private struct KernelMap
{
public uint TextStartOffset;
public uint TextEndOffset;
public uint RodataStartOffset;
public uint RodataEndOffset;
public uint DataStartOffset;
public uint DataEndOffset;
public uint BssStartOffset;
public uint BssEndOffset;
public uint Ini1StartOffset;
public uint DynamicOffset;
public uint InitArrayStartOffset;
public uint InitArrayEndOffset;
}
}
}

View file

@ -8,7 +8,7 @@ namespace LibHac.Kernel
{
public class InitialProcessBinaryReader
{
private const uint ExpectedMagic = 0x31494E49; // INI1
internal const uint ExpectedMagic = 0x31494E49; // INI1
private const int MaxProcessCount = 80;
private IStorage _storage;

View file

@ -36,6 +36,7 @@ namespace hactoolnet
new CliOption("debugoutdir", 1, (o, a) => o.DebugOutDir = a[0]),
new CliOption("savedir", 1, (o, a) => o.SaveOutDir = a[0]),
new CliOption("outdir", 1, (o, a) => o.OutDir = a[0]),
new CliOption("ini1dir", 1, (o, a) => o.Ini1OutDir = a[0]),
new CliOption("outfile", 1, (o, a) => o.OutFile = a[0]),
new CliOption("plaintext", 1, (o, a) => o.PlaintextOut = a[0]),
new CliOption("ciphertext", 1, (o, a) => o.CiphertextOut = a[0]),
@ -60,6 +61,7 @@ namespace hactoolnet
new CliOption("trim", 0, (o, a) => o.TrimSave = true),
new CliOption("readbench", 0, (o, a) => o.ReadBench = true),
new CliOption("hashedfs", 0, (o, a) => o.BuildHfs = true),
new CliOption("extractini1", 0, (o, a) => o.ExtractIni1 = true),
new CliOption("title", 1, (o, a) => o.TitleId = ParseTitleId(a[0])),
new CliOption("bench", 1, (o, a) => o.BenchType = a[0]),
new CliOption("cpufreq", 1, (o, a) => o.CpuFrequencyGhz = ParseDouble(a[0])),
@ -254,6 +256,8 @@ namespace hactoolnet
sb.AppendLine(" --outdir <dir> Specify Package1 directory path.");
sb.AppendLine("Package2 options:");
sb.AppendLine(" --outdir <dir> Specify Package2 directory path.");
sb.AppendLine(" --extractini1 Enable INI1 extraction to default directory (redundant with --ini1dir set).");
sb.AppendLine(" --ini1dir <dir> Specify INI1 directory path. Overrides default path, if present.");
sb.AppendLine("INI1 options:");
sb.AppendLine(" --outdir <dir> Specify INI1 directory path.");
sb.AppendLine("Switch FS options:");

View file

@ -27,6 +27,7 @@ namespace hactoolnet
public string RomfsOutDir;
public string DebugOutDir;
public string SaveOutDir;
public string Ini1OutDir;
public string OutDir;
public string OutFile;
public string PlaintextOut;
@ -54,6 +55,7 @@ namespace hactoolnet
public bool TrimSave;
public bool ReadBench;
public bool BuildHfs;
public bool ExtractIni1;
public ulong TitleId;
public string BenchType;
public double CpuFrequencyGhz;

View file

@ -29,27 +29,31 @@ namespace hactoolnet
{
using (var file = new LocalStorage(ctx.Options.InFile, FileAccess.Read))
{
var ini1 = new InitialProcessBinaryReader();
ini1.Initialize(file).ThrowIfFailure();
string outDir = ctx.Options.OutDir;
if (outDir != null)
{
Directory.CreateDirectory(outDir);
var kipReader = new KipReader();
for (int i = 0; i < ini1.ProcessCount; i++)
{
ini1.OpenKipStorage(out IStorage kipStorage, i).ThrowIfFailure();
kipReader.Initialize(kipStorage).ThrowIfFailure();
kipStorage.WriteAllBytes(Path.Combine(outDir, $"{kipReader.Name.ToString()}.kip1"));
}
ExtractIni1(file, outDir);
}
}
}
public static void ExtractIni1(IStorage iniStorage, string outDir)
{
var ini1 = new InitialProcessBinaryReader();
ini1.Initialize(iniStorage).ThrowIfFailure();
Directory.CreateDirectory(outDir);
var kipReader = new KipReader();
for (int i = 0; i < ini1.ProcessCount; i++)
{
ini1.OpenKipStorage(out IStorage kipStorage, i).ThrowIfFailure();
kipReader.Initialize(kipStorage).ThrowIfFailure();
kipStorage.WriteAllBytes(Path.Combine(outDir, $"{kipReader.Name.ToString()}.kip1"));
}
}
}
}

View file

@ -39,6 +39,12 @@ namespace hactoolnet
ctx.Logger.LogMessage(package2.Print());
string outDir = ctx.Options.OutDir;
string iniDir = ctx.Options.Ini1OutDir;
if (iniDir == null && ctx.Options.ExtractIni1)
{
iniDir = Path.Combine(outDir, "INI1");
}
if (outDir != null)
{
@ -47,12 +53,21 @@ namespace hactoolnet
package2.OpenPayload(out IStorage kernelStorage, 0).ThrowIfFailure();
kernelStorage.WriteAllBytes(Path.Combine(outDir, "Kernel.bin"), ctx.Logger);
package2.OpenPayload(out IStorage ini1Storage, 1).ThrowIfFailure();
package2.OpenIni(out IStorage ini1Storage).ThrowIfFailure();
ini1Storage.WriteAllBytes(Path.Combine(outDir, "INI1.bin"), ctx.Logger);
package2.OpenDecryptedPackage(out IStorage decPackageStorage).ThrowIfFailure();
decPackageStorage.WriteAllBytes(Path.Combine(outDir, "Decrypted.bin"), ctx.Logger);
}
if (iniDir != null)
{
Directory.CreateDirectory(iniDir);
package2.OpenIni(out IStorage ini1Storage).ThrowIfFailure();
ProcessKip.ExtractIni1(ini1Storage, iniDir);
}
}
}