Skip to content

Instantly share code, notes, and snippets.

@d-Rickyy-b
Forked from Slaynash/Program.cs
Created September 14, 2022 02:04
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save d-Rickyy-b/80503bb89a912302b7fbde38af13412b to your computer and use it in GitHub Desktop.
Save d-Rickyy-b/80503bb89a912302b7fbde38af13412b to your computer and use it in GitHub Desktop.
NSISBI Extractor (Unity Installer Extractor)

NSISBI extractor

This is a code draft, and isn't meant to be used in production!

This tool is capable of extracting NSISBI (Nullsoft Scriptable Installer System Big Installer) executables

Requirements

  • LZMA SDK 19.00
  • .net framework 4.8
  • Unsafe code permission (in Visual Studio)

Usage

UnityNSISReader.exe -f -o [-r]

  • r: Filter the results using a regex

Example: UnityNSISReader.exe "-fUnitySetup-Windows-IL2CPP-Support-for-Editor-2020.3.19f1.exe" "-oil2cpp_2020.3.19f1" "-rVariations/.*_il2cpp/UnityPlayer.*(dll|pdb)"

Credits

  • 7-zip: Parts of the source code, licenced under GNU LGPL and/or BSD 3-clause
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.RegularExpressions;
namespace UnityNSISReader
{
class Program
{
private const uint FhFlagsMask = 255;
private const uint FhSig = 0xDEADBEEF;
private const uint FhInt1 = 0x6C6C754E, FhInt2 = 0x74666F73, FhInt3 = 0x74736E49;
private static byte[] Utf8Limits = new[] { (byte)0xC0, (byte)0xE0, (byte)0xF0, (byte)0xF8, (byte)0xFC };
private const uint SignatureSize = 16;
private const uint StartHeaderSize = 28;
private const uint MaskIsCompressed = unchecked ((uint)(1 << 31));
private const uint PageSize = 16 * 4;
private const uint NumCommandParams = 6;
private const uint CommandSize = 4 + NumCommandParams * 4;
private static string sourceFile;
private static string outDir;
private static string filter;
private static int m_length;
private static int m_pos;
private static int g_filehdrsize;
private static bool IsUnicode;
private static uint NumStringChars;
private static unsafe Header* g_header;
private static unsafe byte* pData;
private static unsafe BlockHeader* bhPages;
private static unsafe BlockHeader* bhSections;
private static unsafe BlockHeader* bhEntries;
private static unsafe BlockHeader* bhStrings;
private static unsafe BlockHeader* bhLangTables;
private static unsafe BlockHeader* bhCtlColors;
private static unsafe BlockHeader* bhFonts;
private static unsafe BlockHeader* bhDatas;
private static readonly string[] VarStrings = new[]
{
"CMDLINE",
"INSTDIR",
"OUTDIR",
"EXEDIR",
"LANGUAGE",
"TEMP",
"PLUGINSDIR",
"EXEPATH", // NSIS 2.26+
"EXEFILE", // NSIS 2.26+
"HWNDPARENT",
"_CLICK", // is set from page->clicknext
"_OUTDIR" // NSIS 2.04+
};
private static readonly int NumInternalVars = 20 + VarStrings.Length;
private static readonly string[] PageTypes = new[]
{
"license",
"components",
"directory",
"instfiles",
"uninstConfirm",
"COMPLETED",
"custom"
};
static unsafe int Main(string[] args)
{
foreach (string arg in args)
{
if (arg.StartsWith("-f"))
{
sourceFile = arg.Substring(2);
if (!File.Exists(sourceFile))
throw new FileNotFoundException("Failed to find the file " + sourceFile);
}
else if (arg.StartsWith("-o"))
{
outDir = arg.Substring(2);
Directory.CreateDirectory(outDir);
}
else if (arg.StartsWith("-r"))
{
filter = arg.Substring(2);
}
}
if (string.IsNullOrEmpty(sourceFile) || string.IsNullOrEmpty(outDir))
{
Console.WriteLine("Usage: UnityNSISReader.exe -f<FileName> -o<OutputPath> [-r<regex>]\n\tr: Filter the results using a regex");
return -1;
}
Console.WriteLine("filter: " + filter);
byte[] fba = File.ReadAllBytes(sourceFile);
fixed (byte* f = fba)
{
FirstHeader* h = null;
Console.WriteLine("Checking header");
int left = m_length = fba.Length;
while (left > 0)
{
int l = Math.Min(left, g_filehdrsize != 0 ? 32768 : 512);
byte* temp = f + m_pos;
if (g_filehdrsize == 0)
{
h = (FirstHeader*)temp;
if (
(h->flags & ~FhFlagsMask) == 0 &&
h->siginfo == FhSig &&
h->nsinst2 == FhInt3 &&
h->nsinst1 == FhInt2 &&
h->nsinst0 == FhInt1
) {
g_filehdrsize = m_pos;
Console.WriteLine($"Found nsis data at {g_filehdrsize:X}");
}
}
m_pos += l;
left -= l;
}
if (g_filehdrsize == 0)
{
Console.Error.WriteLine("Failed to locate NSIS data in the target file");
return -1;
}
// INVALID ENDIAN
Console.WriteLine("Installer: " + ((h->flags & (uint)NFlags.kUninstall) == 0));
bool isNSISBI = (h->flags & 0x30) != 0;
Console.WriteLine("NSISBI: " + isNSISBI);
uint headerSize = *(uint*)((byte*)h + SignatureSize + 4);
uint arcSize = *(uint*)((byte*)h + SignatureSize + 8);
Console.WriteLine("Header Size: " + headerSize);
Console.WriteLine("Arc Size: " + arcSize);
if (arcSize <= StartHeaderSize)
{
Console.Error.WriteLine("Arc size is less than StartHeaderSize. This should not happen !");
return -1;
}
bool headerIsCompressed = true;
uint dictionarySize = 1;
bool isSolid = true;
uint nonSolidStartOffset = 0;
byte* sig = (byte*)h + SignatureSize + 8 + 4;
if (isNSISBI)
sig += 8;
uint compressedHeaderSize = *(uint*)(sig);
Console.WriteLine("Compressed Header Size: " + compressedHeaderSize.ToString("X4"));
if (compressedHeaderSize == headerSize)
{
isSolid = false;
headerIsCompressed = false;
Console.WriteLine("Header is NOT compressed");
}
else if (IsLZMA(sig, ref dictionarySize))
{
Console.WriteLine("Header is LZMA compressed (Solid)");
}
else if (sig[3] == 0x80)
{
isSolid = false;
if (IsLZMA(sig + 4, ref dictionarySize) && sig[3] == 0x80)
Console.WriteLine("Header is LZMA compressed (Non-Solid)");
else if (IsBZip2(sig + 4))
Console.WriteLine("Header is BZip2 compressed (Non-Solid)");
else
Console.WriteLine("Header is Deflate compressed (Non-Solid)");
}
else if (IsBZip2(sig))
Console.WriteLine("Header is BZip2 compressed (Solid)");
else
{
Console.WriteLine("Header is Deflate compressed (Solid)");
sig -= 4;
}
if (!isSolid)
{
headerIsCompressed = ((compressedHeaderSize & MaskIsCompressed) != 0);
compressedHeaderSize &= ~MaskIsCompressed;
nonSolidStartOffset = compressedHeaderSize;
}
byte[] _data = new byte[headerSize];
byte[] buffer = new byte[arcSize]; // -44 ?
Marshal.Copy((IntPtr)(sig + 4), buffer, 0, (int)arcSize);
//using (DeflateStream ds = new DeflateStream(new MemoryStream(buffer), CompressionMode.Decompress))
/*
using (GZipStream ds = new GZipStream(new MemoryStream(buffer), CompressionMode.Decompress))
{
if (isSolid)
{
byte[] buf = new byte[4];
ds.Read(buf, 0, 4);
uint bufValue = BitConverter.ToUInt32(buf, 0);
if (bufValue != headerSize)
{
Console.Error.WriteLine("Solid size mismatch header size (" + bufValue + " vs " + headerSize + ")");
return;
}
Console.Error.WriteLine("bufValue is valid");
}
ds.Read(_data, 0, (int)headerSize);
}
*/
SevenZip.Compression.LZMA.Decoder lzmadecoder = new SevenZip.Compression.LZMA.Decoder();
byte[] properties = new byte[5];
Array.Copy(buffer, properties, 5);
lzmadecoder.SetDecoderProperties(properties);
//long fileLength = BitConverter.ToInt64(buffer, 5);
//Console.WriteLine("[LZMA] fileLength: " + fileLength);
MemoryStream outputStream = new MemoryStream(1 << 20);
lzmadecoder.Code(new MemoryStream(buffer, 5, buffer.Length - 5), outputStream, buffer.Length - 5, 1 << 20, null);
if (isSolid)
{
byte[] buf = new byte[4];
outputStream.Read(buf, 0, 4);
uint bufValue = BitConverter.ToUInt32(buf, 0);
if (bufValue != headerSize)
{
Console.Error.WriteLine("Solid size mismatch header size (" + bufValue + " vs " + headerSize + ")");
return -1;
}
Console.Error.WriteLine("bufValue is valid");
_data = new byte[outputStream.Length - 4];
outputStream.Read(_data, 4, (int)outputStream.Length - 4);
}
else
_data = outputStream.ToArray();
fixed (byte* p = _data)
{
pData = p;
bhPages = (BlockHeader*)(p + 4 + 8 * 0);
bhSections = (BlockHeader*)(p + 4 + 8 * 1);
bhEntries = (BlockHeader*)(p + 4 + 8 * 2);
bhStrings = (BlockHeader*)(p + 4 + 8 * 3);
bhLangTables = (BlockHeader*)(p + 4 + 8 * 4);
bhCtlColors = (BlockHeader*)(p + 4 + 8 * 5);
bhFonts = (BlockHeader*)(p + 4 + 8 * 6);
bhDatas = (BlockHeader*)(p + 4 + 8 * 7);
Console.WriteLine("Entries: " + bhEntries->num + " starting at " + bhEntries->offset + " (Valid: " + (bhEntries->offset < headerSize) + ")");
Console.WriteLine("Strings: " + bhStrings->num + " starting at " + bhStrings->offset + " (Valid: " + (bhStrings->offset < headerSize) + ")");
Console.WriteLine("LangTables: " + bhLangTables->num + " starting at " + bhLangTables->offset + " (Valid: " + (bhLangTables->offset < headerSize) + ")");
if (
bhEntries->offset > headerSize ||
bhStrings->offset > headerSize ||
bhLangTables->offset > headerSize
)
{
Console.Error.WriteLine("BlockHeader offsets are out of bounds!");
return -1;
}
if (bhLangTables->offset < bhStrings->offset)
{
Console.Error.WriteLine("bhLangTables is before bhStrings");
return -1;
}
uint stringTableSize = bhLangTables->offset - bhStrings->offset;
Console.WriteLine("stringTableSize: " + stringTableSize);
if (stringTableSize < 2)
{
Console.Error.WriteLine("stringTableSize is less than 2 (" + stringTableSize + ")");
return -1;
}
byte* strData = p + bhStrings->offset;
if (strData[stringTableSize - 1] != 0)
{
Console.Error.WriteLine("byte at stringTableSize-1 is not 0");
return -1;
}
IsUnicode = *(short*)strData == 0;
Console.WriteLine("Unicode: " + IsUnicode + " (" + *(short*)strData + ")");
NumStringChars = stringTableSize;
if (IsUnicode)
{
if ((stringTableSize & 1) != 0)
{
Console.Error.WriteLine("(stringTableSize & 1) is not 0");
return -1;
}
NumStringChars >>= 1;
if ((strData[stringTableSize - 2]) != 0)
{
Console.Error.WriteLine("(strData[stringTableSize - 2]) is not 0");
return -1;
}
}
Console.WriteLine("NumStringChars: " + NumStringChars);
if (bhEntries->num > (1 << 25))
{
Console.Error.WriteLine("bhEntries is too big (1)");
return -1;
}
if (bhEntries->num * 28 > headerSize - bhEntries->offset)
{
Console.Error.WriteLine("bhEntries is too big (2)");
return -1;
}
Console.WriteLine("Done checking header");
Console.WriteLine();
byte* cmdPtr = null;
Console.WriteLine();
// Commands
cmdPtr = p + bhEntries->offset;
uint commandSize = CommandSize;
if (isNSISBI)
commandSize += 2 * 4;
string currentOutPath = "";
List<string> files = new List<string>();
for (int kkk = 0; kkk < bhEntries->num; kkk++, cmdPtr += commandSize)
{
uint commandId = *(uint*)cmdPtr; // TODO Handle commands shifting depending on the NSIS version
uint[] _params = new uint[NumCommandParams];
for (int i = 0; i < NumCommandParams; ++i)
_params[i] = *(uint*)(cmdPtr + 4 + 4 * i);
switch (commandId)
{
case 11:
bool isSetOutPath = (_params[1] != 0);
/*
string s = "";
if (isSetOutPath)
{
uint par0 = _params[0];
uint resOffset = 0;
int idx = GetVarIndex(par0, ref resOffset);
if (idx == 31 || idx == 22)
par0 += resOffset;
s += ReadString2_Raw(par0);
}
*/
//Console.Write(isSetOutPath ? "SetOutPath" : "CreateDirectory");
//string path = ReadString2(_params[0]);
//if (isSetOutPath)
// currentOutPath = path;
//Console.Write(" " + path);
//if (_params[2] != 0)
// Console.Write(" ; CreateRestrictedDirectory");
string outPath = ReadString2(_params[0]).Replace("$_OUTDIR\\", "").Replace("$_OUTDIR", "").Replace("$INSTDIR\\", "");
if (outPath == "$_OUTDIR" || outPath == "$INSTDIR")
outPath = "";
//Console.WriteLine(" --- > " + outPath);
if (outPath.EndsWith("/"))
outPath = outPath.Substring(0, outPath.Length - 1);
currentOutPath = outPath;
/*
if (!Directory.Exists(currentOutPath))
Directory.CreateDirectory(currentOutPath);
*/
continue;
case 20:
//Console.Write("File");
string fileName = ReadString2(_params[1]);
//Console.Write(" " + fileName);
uint offset = _params[2];
//Console.Write(" @ " + offset);
if (isSolid)
throw new NotImplementedException("Can decompress file in a Solid compression type");
byte* fileData = sig + nonSolidStartOffset + 4 + _params[2];
uint size = *(uint*)(fileData);
bool isFileCompressed = false;
if ((size & MaskIsCompressed) != 0)
{
isFileCompressed = true;
size &= ~MaskIsCompressed;
}
//Console.Write($" (size: {size} [compressed: {isFileCompressed}])");
string filePathLocal = Path.Combine(currentOutPath, fileName).Replace('\\', '/');
if (files.Contains(filePathLocal))
continue;
files.Add(filePathLocal);
if (!string.IsNullOrEmpty(filter) && !Regex.IsMatch(filePathLocal, filter))
continue;
Console.WriteLine(filePathLocal);
string filePath = Path.Combine(outDir, filePathLocal);
string filePathParent = Directory.GetParent(filePath).FullName;
//Console.Write($" (filePathReal: {filePathReal})");
Directory.CreateDirectory(filePathParent);
if (File.Exists(filePath))
File.Delete(filePath);
//Console.WriteLine();
byte[] data = new byte[size];
if (isFileCompressed)
{
Marshal.Copy((IntPtr)fileData, data, 0, (int)size);
SevenZip.Compression.LZMA.Decoder lzmadecoder2 = new SevenZip.Compression.LZMA.Decoder();
byte[] properties2 = new byte[5];
Array.Copy(data, 4, properties, 0, 5);
lzmadecoder2.SetDecoderProperties(properties);
//long fileLength2 = BitConverter.ToInt64(data, 4 + 5);
//Console.WriteLine("[LZMA] fileLength: " + fileLength2);
MemoryStream outputStream2 = new MemoryStream(1 << 20);
lzmadecoder2.Code(new MemoryStream(data, 4 + 5, data.Length - (4 + 5)), outputStream2, data.Length - (4 + 5), 1 << 28, null);
if (isSolid)
{
/*
byte[] buf = new byte[4];
outputStream.Read(buf, 0, 4);
uint bufValue = BitConverter.ToUInt32(buf, 0);
if (bufValue != headerSize)
{
Console.Error.WriteLine("Solid size mismatch header size (" + bufValue + " vs " + headerSize + ")");
return;
}
Console.Error.WriteLine("bufValue is valid");
*/
_data = new byte[outputStream2.Length];
outputStream.Read(_data, 4, (int)outputStream2.Length);
}
else
_data = outputStream2.ToArray();
}
else
{
Marshal.Copy((IntPtr)(fileData + 4), data, 0, (int)size);
_data = data;
}
File.WriteAllBytes(filePath, _data);
continue;
}
/*
Console.Write(commandId);
for (int i = 0; i < NumCommandParams; ++i)
{
Console.Write(" ");
Console.Write(ReadString2(_params[i]));
}
Console.Write(" ;");
for (int i = 0; i < NumCommandParams; ++i)
{
Console.Write(" " + (int)_params[i]);
}
Console.WriteLine();
*/
/*
switch (commandId)
{
case NsisCommands.CreateDir:
break;
}
*/
}
}
/*
uint inputLen = *(uint*)(f + g_filehdrsize + sizeof(FirstHeader));
Console.WriteLine("Input Length is " + inputLen + ". Compressed: " + ((inputLen & 0x80000000) > 0));
byte* data = f + g_filehdrsize + sizeof(FirstHeader) + sizeof(int);
Header* header = g_header = (Header*)data;
header->blockSections.offset += (int)data;
header->blockEntries.offset += (int)data;
header->blockStrings.offset += (int)data;
header->blockLangTables.offset += (int)data;
header->blockCtlColors.offset += (int)data;
header->blockData.offset += (int)data;
Console.WriteLine("installRegKeyPtr: " + header->installRegKeyPtr);
Console.WriteLine("Str: " + GetNSISString(header->installDirectoryPtr));
*/
}
Console.WriteLine();
return 0;
}
private static unsafe string ReadString2(uint pos)
{
if (pos < 0)
return $"$(LSTR_{-(pos + 1)})";
else if (pos >= NumStringChars)
return "$_ERROR_STR_" + pos;
else
{
if (IsUnicode)
return GetNsisStringUnicode(pData + bhStrings->offset + pos * 2);
else
return GetNsisString(pData + bhStrings->offset + pos);
}
}
private static string ReadString2_Raw(uint pos)
{
if (pos < 0)
return $"$(LSTR_{-(pos + 1)})";
else if (pos >= NumStringChars)
return "$_ERROR_STR_" + pos;
else
{
return $"S_{pos}";
}
}
private static unsafe string GetNsisStringUnicode(byte* p)
{
StringBuilder sb = new StringBuilder();
for (;;)
{
ushort c = *(ushort*)p;
p += 2;
//Console.Write($"[C{c:X4}]");
if (c == 0)
break;
if (false /*IsPark()*/)
{
if (c >= 0xE000 && c <= 0xE003)
{
ushort n = *(ushort*)p;
p += 2;
if (n == 0)
break;
if (c != 0xE000)
{
if (c == 0xE002)
sb.Append($"$(SSTR_{n & 0xFF}_{n >> 8})");
else
{
n &= 0x7FFF;
if (c == 0xE001)
sb.Append(GetVar(n));
else
sb.Append($"$(LSTR_{n})");
}
continue;
}
c = n;
}
}
else
{
if (c <= 4)
{
ushort n = *(ushort*)p;
//Console.Write($"[N{n:X4}]");
p += 2;
if (n == 0)
break;
if (c != 4)
{
if (c == 2)
sb.Append($"$(SSTR_{n & 0xFF}_{n >> 8})");
else
{
n = (ushort)((n & 0x7F) | (((ushort)(n >> 8) & 0x7F) << 7));
if (c == 3)
sb.Append(GetVar(n));
else
sb.Append($"$(LSTR_{n})");
}
continue;
}
c = n;
}
}
if (c < 0x80)
{
if (c == 9) sb.Append("$\\t");
else if (c == 10) sb.Append("$\\n");
else if (c == 13) sb.Append("$\\r");
else if (c == '"') sb.Append("$\\\"");
else if (c == '$') sb.Append("$$");
else sb.Append((char)c, 1);
//Console.Write($"[A:{(char)c}]");
continue;
}
short numAdds;
for (numAdds = 1; numAdds < 5; ++numAdds)
if (c < (1 << (numAdds * 5 + 6)))
{
//Console.Write($"[NAB]");
break;
}
sb.Append((char)(Utf8Limits[numAdds - 1] + (c >> (6 * numAdds))));
do
{
numAdds--;
sb.Append((char)(0x80 + ((c >> (6 * numAdds)) & 0x3F)));
}
while (numAdds != 0);
}
return sb.ToString();
}
private static unsafe string GetNsisString(byte* s)
{
string tmp = "";
for (;;)
{
byte c = *s++;
if (c == 0)
return tmp;
if (c <= 4)
{
byte c0 = *s++;
if (c0 == 0)
return tmp;
if (c != 4)
{
byte c1 = *s++;
if (c1 == 0)
return tmp;
if (c1 == 4)
tmp += $"$(SSTR_{c0}_{c1})";
else
{
uint n = (uint)((ushort)(c0 & 0x7F) | ((ushort)(c1 & 0x7F) << 7));
if (c == 3)
tmp += GetVar(n);
else
tmp += $"$(LSTR_{n})";
}
continue;
}
}
}
}
private static string GetVar(uint index)
{
return "$" + GetVar2(index);
}
private static string GetVar2(uint index)
{
string tmp = "";
if (index < 20)
{
if (index >= 10)
{
tmp += "R";
index -= 10;
}
tmp += index;
}
else
{
if (index < NumInternalVars)
{
tmp += VarStrings[index - 20];
}
else
{
tmp += $"_{index - NumInternalVars}_";
}
}
return tmp;
}
private static int GetVarIndex(uint strPos, ref uint resOffset)
{
resOffset = 0;
int varIndex = GetVarIndex(strPos);
if (varIndex < 0)
return varIndex;
if (IsUnicode)
{
if (NumStringChars - strPos < 2 * 2)
return -1;
resOffset = 2;
}
else
{
if (NumStringChars - strPos < 3)
return -1;
resOffset = 3;
}
return varIndex;
}
private static int GetVarIndex(uint strPos)
{
/*
if (strPos >= NumStringChars)
return -1;
if (IsUnicode)
{
if (NumStringChars - strPos < 3 * 2)
return -1;
byte* p = _data
}
*/
return -2;
}
private static unsafe bool IsLZMA_(byte* p, ref uint dictionary)
{
dictionary = *(uint*)(p + 1);
return p[0] == 0x5D &&
p[1] == 0x00 && p[2] == 0x00 &&
p[5] == 0x00 && (p[6] & 0x80) == 0x00;
}
private static unsafe bool IsBZip2(byte* p)
{
return (p[0] == 0x31 && p[1] < 14);
}
private static unsafe bool IsLZMA(byte* p, ref uint dictionary)
{
if (IsLZMA_(p, ref dictionary))
return true;
if (*(uint*)p <= 1 && IsLZMA_(p + 1, ref dictionary))
return true;
return false;
}
private static unsafe string GetNSISString(int strtab)
{
char* data = (char*)(g_header->blockStrings.offset + strtab);
return new string(data);
}
public static int SwapEndianness(int value)
{
var b1 = (value >> 0) & 0xff;
var b2 = (value >> 8) & 0xff;
var b3 = (value >> 16) & 0xff;
var b4 = (value >> 24) & 0xff;
return b1 << 24 | b2 << 16 | b3 << 8 | b4 << 0;
}
}
[StructLayout(LayoutKind.Sequential)]
struct FirstHeader
{
public uint flags;
public uint siginfo;
public int nsinst0, nsinst1, nsinst2;
public int length_of_header;
public int length_of_all_following_data;
}
enum NFlags : uint
{
kUninstall = 1,
kSilent = 2,
kNoCrc = 4,
kForceCrc = 8,
}
[StructLayout(LayoutKind.Sequential)]
struct Header
{
public uint flags;
public BlockHeader blockSections;
public BlockHeader blockEntries;
public BlockHeader blockStrings;
public BlockHeader blockLangTables;
public BlockHeader blockCtlColors;
public BlockHeader blockData;
public int installRegRootkey;
public int installRegKeyPtr, installRegValuePtr;
}
struct BlockHeader
{
public uint offset;
public uint num;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment