249 lines
8.7 KiB
C#
249 lines
8.7 KiB
C#
![]() |
/*
|
||
|
Copyright (C) 2014-2019 de4dot@gmail.com
|
||
|
|
||
|
This file is part of dnSpy
|
||
|
|
||
|
dnSpy is free software: you can redistribute it and/or modify
|
||
|
it under the terms of the GNU General Public License as published by
|
||
|
the Free Software Foundation, either version 3 of the License, or
|
||
|
(at your option) any later version.
|
||
|
|
||
|
dnSpy is distributed in the hope that it will be useful,
|
||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
|
GNU General Public License for more details.
|
||
|
|
||
|
You should have received a copy of the GNU General Public License
|
||
|
along with dnSpy. If not, see <http://www.gnu.org/licenses/>.
|
||
|
*/
|
||
|
|
||
|
using System;
|
||
|
using System.Collections.Generic;
|
||
|
using System.Diagnostics.CodeAnalysis;
|
||
|
using System.IO;
|
||
|
using System.Security.Cryptography;
|
||
|
using System.Text;
|
||
|
using dnSpy.Debugger.DotNet.CorDebug.Utilities;
|
||
|
|
||
|
namespace dnSpy.Debugger.DotNet.CorDebug.Impl {
|
||
|
static class AppHostUtils {
|
||
|
static int CalculateMaxAppHostExeSize() {
|
||
|
ulong maxSize = 0;
|
||
|
foreach (var info in AppHostInfoData.KnownAppHostInfos) {
|
||
|
maxSize = Math.Max(maxSize, (ulong)info.HashDataOffset + info.HashDataSize);
|
||
|
maxSize = Math.Max(maxSize, (ulong)info.RelPathOffset + AppHostInfo.MaxAppHostRelPathLength);
|
||
|
}
|
||
|
return checked((int)maxSize);
|
||
|
}
|
||
|
|
||
|
static readonly int MaxAppHostExeSize = CalculateMaxAppHostExeSize();
|
||
|
const string AppHostExeUnpatched = "c3ab8ff13720e8ad9047dd39466b3c89" + "74e592c2fa383d4a3960714caef0c4f2";
|
||
|
static readonly byte[] AppHostExeUnpatchedSignature = Encoding.UTF8.GetBytes(AppHostExeUnpatched + "\0");
|
||
|
static readonly byte[] AppHostExeSignature = Encoding.UTF8.GetBytes("c3ab8ff13720e8ad9047dd39466b3c89" + "\0");
|
||
|
|
||
|
// .NET Core 2.x, older .NET Core 3.0 previews
|
||
|
internal static bool IsDotNetAppHostV1(string filename, [NotNullWhen(true)] out string? dllFilename) {
|
||
|
// We detect the apphost.exe like so:
|
||
|
// - must have an exe extension
|
||
|
// - must be a PE file and an EXE (DLL bit cleared)
|
||
|
// - must not have .NET metadata
|
||
|
// - must have a file with the same name but a dll extension
|
||
|
// - this dll file must be a PE file and have .NET metadata
|
||
|
|
||
|
// .NET Core 1.x: the apphost is a renamed dotnet.exe and it assumes (unless overridden
|
||
|
// on the command line) that the managed dll is apphostname with a dll extension.
|
||
|
// .NET Core 2.x-3.x: the relative path of the managed dll is part of the exe, patched
|
||
|
// by an MSBuild task. Max utf8 string length is 1024 bytes. It's currently not possible
|
||
|
// to override this path so it should be identical to apphostname with a dll extension,
|
||
|
// unless someone patched the apphost exe (eg. dnSpy).
|
||
|
|
||
|
// Windows apphosts have an .exe extension. Don't call Path.ChangeExtension() unless it's guaranteed
|
||
|
// to have an .exe extension, eg. 'some.file' => 'some.file.dll', not 'some.dll'
|
||
|
if (filename.EndsWith(".exe", StringComparison.OrdinalIgnoreCase))
|
||
|
dllFilename = Path.ChangeExtension(filename, ".dll");
|
||
|
else if (!filename.EndsWith(".dll", StringComparison.OrdinalIgnoreCase))
|
||
|
dllFilename = filename + ".dll";
|
||
|
else {
|
||
|
dllFilename = null;
|
||
|
return false;
|
||
|
}
|
||
|
if (!File.Exists(dllFilename))
|
||
|
return false;
|
||
|
if (PortableExecutableFileHelpers.IsPE(filename, out bool isExe, out bool hasDotNetMetadata) && (!isExe || hasDotNetMetadata))
|
||
|
return false;
|
||
|
if (!PortableExecutableFileHelpers.IsPE(dllFilename, out _, out hasDotNetMetadata) || !hasDotNetMetadata)
|
||
|
return false;
|
||
|
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
// Older .NET Core 3.0 previews
|
||
|
internal static bool IsDotNetBundleV1(string filename) {
|
||
|
try {
|
||
|
using (var stream = File.OpenRead(filename)) {
|
||
|
if (stream.Length < bundleSig.Length)
|
||
|
return false;
|
||
|
stream.Position = stream.Length - bundleSig.Length;
|
||
|
var sig = new byte[bundleSig.Length];
|
||
|
stream.Read(sig, 0, sig.Length);
|
||
|
for (int i = 0; i < sig.Length; i++) {
|
||
|
if (bundleSig[i] != sig[i])
|
||
|
return false;
|
||
|
}
|
||
|
return true;
|
||
|
}
|
||
|
}
|
||
|
catch {
|
||
|
}
|
||
|
return false;
|
||
|
}
|
||
|
// "\x0E.NetCoreBundle"
|
||
|
static readonly byte[] bundleSig = new byte[] { 0x0E, 0x2E, 0x4E, 0x65, 0x74, 0x43, 0x6F, 0x72, 0x65, 0x42, 0x75, 0x6E, 0x64, 0x6C, 0x65 };
|
||
|
|
||
|
// .NET Core 3.0
|
||
|
internal static bool IsDotNetBundleV2_or_AppHost(string filename) {
|
||
|
try {
|
||
|
var data = ReadBytes(filename, 512 * 1024);
|
||
|
return GetOffset(data, apphostSigV2) >= 0;
|
||
|
}
|
||
|
catch {
|
||
|
}
|
||
|
return false;
|
||
|
}
|
||
|
// SHA256(".net core bundle"). The previous 8 bytes is 0 or offset of the bundle header-offset
|
||
|
static readonly byte[] apphostSigV2 = new byte[] {
|
||
|
0x8B, 0x12, 0x02, 0xB9, 0x6A, 0x61, 0x20, 0x38, 0x72, 0x7B, 0x93, 0x02, 0x14, 0xD7, 0xA0, 0x32,
|
||
|
0x13, 0xF5, 0xB9, 0xE6, 0xEF, 0xAE, 0x33, 0x18, 0xEE, 0x3B, 0x2D, 0xCE, 0x24, 0xB3, 0x6A, 0xAE,
|
||
|
};
|
||
|
|
||
|
static byte[] ReadBytes(string filename, int maxBytes) {
|
||
|
using (var file = File.OpenRead(filename)) {
|
||
|
int size = (int)Math.Min(file.Seek(0, SeekOrigin.End), maxBytes);
|
||
|
file.Position = 0;
|
||
|
var data = new byte[size];
|
||
|
int sizeRead = file.Read(data, 0, data.Length);
|
||
|
if (sizeRead != data.Length)
|
||
|
throw new IOException($"Wanted to read {data.Length} bytes, but could only read {sizeRead} bytes, file '{filename}'");
|
||
|
return data;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
internal static bool TryGetAppHostEmbeddedDotNetDllPath(string apphostFilename, out bool couldBeAppHost, [NotNullWhen(true)] out string? dotNetDllPath) {
|
||
|
dotNetDllPath = null;
|
||
|
couldBeAppHost = false;
|
||
|
if (!File.Exists(apphostFilename))
|
||
|
return false;
|
||
|
if (PortableExecutableFileHelpers.IsPE(apphostFilename, out _, out var hasDotNetMetadata) && hasDotNetMetadata)
|
||
|
return false;
|
||
|
try {
|
||
|
var data = ReadBytes(apphostFilename, MaxAppHostExeSize);
|
||
|
if (GetOffset(data, AppHostExeUnpatchedSignature) >= 0) {
|
||
|
couldBeAppHost = true;
|
||
|
return false;
|
||
|
}
|
||
|
if (GetOffset(data, AppHostExeSignature) < 0)
|
||
|
return false;
|
||
|
couldBeAppHost = true;
|
||
|
if (!ExeUtils.TryGetTextSectionInfo(new BinaryReader(new MemoryStream(data)), out _, out _))
|
||
|
return false;
|
||
|
|
||
|
var basePath = Path.GetDirectoryName(apphostFilename)!;
|
||
|
foreach (var info in GetAppHostInfos(data)) {
|
||
|
if (!TryGetUtf8StringZ(data, (int)info.RelPathOffset, AppHostInfo.MaxAppHostRelPathLength, out var relPath))
|
||
|
continue;
|
||
|
if (relPath == AppHostExeUnpatched)
|
||
|
continue;
|
||
|
string dotnetFile;
|
||
|
try {
|
||
|
dotnetFile = Path.Combine(basePath, relPath);
|
||
|
}
|
||
|
catch (ArgumentException) {
|
||
|
continue;
|
||
|
}
|
||
|
if (!PortableExecutableFileHelpers.IsPE(dotnetFile, out _, out hasDotNetMetadata))
|
||
|
continue;
|
||
|
if (!hasDotNetMetadata)
|
||
|
continue;
|
||
|
dotNetDllPath = dotnetFile;
|
||
|
return true;
|
||
|
}
|
||
|
}
|
||
|
catch (IOException) {
|
||
|
}
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
static bool TryGetUtf8StringZ(byte[] data, int index, int maxLength, [NotNullWhen(true)] out string? relPath) {
|
||
|
for (int i = 0; i < maxLength && (uint)(index + i) < (uint)data.Length; i++) {
|
||
|
if (data[index + i] == 0) {
|
||
|
relPath = Encoding.UTF8.GetString(data, index, i);
|
||
|
return true;
|
||
|
}
|
||
|
}
|
||
|
relPath = null;
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
static IEnumerable<AppHostInfo> GetAppHostInfos(byte[] data) {
|
||
|
uint hashDataOffset = 0, hashDataSize = 0;
|
||
|
byte[]? hash = null;
|
||
|
foreach (var info in AppHostInfoData.KnownAppHostInfos) {
|
||
|
if (hash is null || !(hashDataOffset == info.HashDataOffset && hashDataSize == info.HashDataSize)) {
|
||
|
if (Hash(data, info.HashDataOffset, info.HashDataSize, info.LastByte) is byte[] newHash) {
|
||
|
hash = newHash;
|
||
|
hashDataOffset = info.HashDataOffset;
|
||
|
hashDataSize = info.HashDataSize;
|
||
|
}
|
||
|
}
|
||
|
if (hash is not null && ByteArrayEquals(hash, info.Hash))
|
||
|
yield return info;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
static byte[]? Hash(byte[] data, uint offset, uint size, byte lastByte) {
|
||
|
if ((ulong)offset + size > (ulong)data.Length)
|
||
|
return null;
|
||
|
if (data[(int)(offset + size - 1)] != lastByte)
|
||
|
return null;
|
||
|
using (var sha1 = new SHA1Managed())
|
||
|
return sha1.ComputeHash(data, (int)offset, (int)size);
|
||
|
}
|
||
|
|
||
|
static bool ByteArrayEquals(byte[]? a, byte[]? b) {
|
||
|
if (a is null || b is null)
|
||
|
return false;
|
||
|
if (a.Length != b.Length)
|
||
|
return false;
|
||
|
for (int i = 0; i < a.Length; i++) {
|
||
|
if (a[i] != b[i])
|
||
|
return false;
|
||
|
}
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
static int GetOffset(byte[] bytes, byte[] pattern) {
|
||
|
int si = 0;
|
||
|
var b = pattern[0];
|
||
|
while (si < bytes.Length) {
|
||
|
si = Array.IndexOf(bytes, b, si);
|
||
|
if (si < 0)
|
||
|
break;
|
||
|
if (Match(bytes, si, pattern))
|
||
|
return si;
|
||
|
si++;
|
||
|
}
|
||
|
return -1;
|
||
|
}
|
||
|
|
||
|
static bool Match(byte[] bytes, int index, byte[] pattern) {
|
||
|
if (index + pattern.Length > bytes.Length)
|
||
|
return false;
|
||
|
for (int i = 0; i < pattern.Length; i++) {
|
||
|
if (bytes[index + i] != pattern[i])
|
||
|
return false;
|
||
|
}
|
||
|
return true;
|
||
|
}
|
||
|
}
|
||
|
}
|