├── src ├── HackF5.UnitySpy.snk ├── HackF5.UnitySpy │ ├── Offsets │ │ ├── BinaryFormat.cs │ │ ├── MachOFormatOffsets.cs │ │ ├── PEFormatOffsets.cs │ │ ├── UnityVersion.cs │ │ └── MonoLibraryOffsets.cs │ ├── Detail │ │ ├── MonoClassKind.cs │ │ ├── MemoryObject.cs │ │ ├── ManagedObjectInstance.cs │ │ ├── ManagedClassInstance.cs │ │ ├── ManagedStructInstance.cs │ │ ├── TypeInfo.cs │ │ ├── TypeCode.cs │ │ ├── FieldDefinition.cs │ │ ├── AssemblyImage.cs │ │ └── TypeDefinition.cs │ ├── IMemoryObject.cs │ ├── ProcessFacade │ │ ├── ModuleInfo.cs │ │ ├── ProcessFacadeMacOS.cs │ │ ├── ProcessFacadeLinuxClient.cs │ │ ├── ProcessFacadeLinuxDirect.cs │ │ ├── ProcessFacadeMacOSClient.cs │ │ ├── ProcessFacadeLinuxPTrace.cs │ │ ├── UnityProcessFacade.cs │ │ ├── ProcessFacadeMacOSDirect.cs │ │ ├── ProcessFacadeLinuxDump.cs │ │ ├── ProcessFacadeLinux.cs │ │ ├── ProcessFacadeWindows.cs │ │ ├── ProcessFacadeClient.cs │ │ └── ProcessFacade.cs │ ├── IFieldDefinition.cs │ ├── HackF5.UnitySpy.csproj │ ├── IAssemblyImage.cs │ ├── ITypeInfo.cs │ ├── Util │ │ ├── ConversionUtils.cs │ │ ├── ByteArrayPool.cs │ │ └── MemoryReadingUtils.cs │ ├── IManagedObjectInstance.cs │ ├── ITypeDefinition.cs │ └── AssemblyImageFactory.cs └── mtga-tracker-daemon │ ├── Models │ ├── ErrorResponseData.cs │ ├── ShutdownResponseData.cs │ ├── CheckForUpdatesResponseData.cs │ ├── EventsResponseData.cs │ ├── InventoryResponseData.cs │ ├── StatusResponseData.cs │ ├── PlayerIDResponseData.cs │ ├── CardsResponseData.cs │ ├── AllCardsResponseData.cs │ └── MatchStateResponseData.cs │ ├── Asset.cs │ ├── DaemonVersion.cs │ ├── StringUtils.cs │ ├── mtga-tracker-daemon.csproj │ ├── Program.cs │ └── HttpServer.cs ├── deploy └── linux │ ├── uninstall.sh │ ├── mtga-trackerd.service │ └── install.sh ├── .gitignore ├── getVersion.js ├── .github └── workflows │ ├── test.yml │ └── release.yml ├── README.md └── mtga-tracker-daemon.sln /src/HackF5.UnitySpy.snk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frcaton/mtga-tracker-daemon/HEAD/src/HackF5.UnitySpy.snk -------------------------------------------------------------------------------- /src/HackF5.UnitySpy/Offsets/BinaryFormat.cs: -------------------------------------------------------------------------------- 1 | namespace HackF5.UnitySpy.Offsets 2 | { 3 | public enum BinaryFormat 4 | { 5 | PE, 6 | MachO, 7 | } 8 | } -------------------------------------------------------------------------------- /src/mtga-tracker-daemon/Models/ErrorResponseData.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace MTGATrackerDaemon.Models 4 | { 5 | public class ErrorResponseData 6 | { 7 | [JsonProperty("error")] 8 | public string Error { get; set; } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /deploy/linux/uninstall.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | SERVICE_NAME=mtga-trackerd.service 4 | 5 | sudo systemctl stop $SERVICE_NAME 6 | sudo systemctl disable $SERVICE_NAME 7 | 8 | rm -rf /usr/share/mtga-tracker-daemon 9 | rm /etc/systemd/system/$SERVICE_NAME 10 | 11 | systemctl daemon-reload -------------------------------------------------------------------------------- /src/HackF5.UnitySpy/Detail/MonoClassKind.cs: -------------------------------------------------------------------------------- 1 | namespace HackF5.UnitySpy.Detail 2 | { 3 | public enum MonoClassKind 4 | { 5 | Def = 1, 6 | GTg = 2, 7 | GInst = 3, 8 | GParam = 4, 9 | Array = 5, 10 | Pointer = 6, 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/mtga-tracker-daemon/Models/ShutdownResponseData.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace MTGATrackerDaemon.Models 4 | { 5 | public class ShutdownResponseData 6 | { 7 | [JsonProperty("result")] 8 | public string Result { get; set; } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/mtga-tracker-daemon/Models/CheckForUpdatesResponseData.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace MTGATrackerDaemon.Models 4 | { 5 | public class CheckForUpdatesResponseData 6 | { 7 | [JsonProperty("updatesAvailable")] 8 | public bool UpdatesAvailable; 9 | } 10 | } -------------------------------------------------------------------------------- /deploy/linux/mtga-trackerd.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description = MTG Arena tracker daemon 3 | 4 | [Service] 5 | User = root 6 | WorkingDirectory = /usr/share/mtga-tracker-daemon 7 | ExecStart = /usr/share/mtga-tracker-daemon/mtga-tracker-daemon 8 | Restart = always 9 | 10 | [Install] 11 | WantedBy = multi-user.target -------------------------------------------------------------------------------- /src/mtga-tracker-daemon/Models/EventsResponseData.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace MTGATrackerDaemon.Models 4 | { 5 | public class EventsResponseData 6 | { 7 | [JsonProperty("events")] 8 | public string[] Events { get; set; } 9 | 10 | [JsonProperty("elapsedTime")] 11 | public int ElapsedTime { get; set; } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # ===================================== # 2 | # Visual Studio / MonoDevelop generated # 3 | # ===================================== # 4 | obj/ 5 | bin/ 6 | *.userprefs 7 | *.pidb 8 | *.suo 9 | *.user 10 | .vscode/ 11 | 12 | # ============ # 13 | # OS generated # 14 | # ============ # 15 | .DS_Store 16 | .DS_Store? 17 | ._* 18 | .Spotlight-V100 19 | .Trashes 20 | ehthumbs.db 21 | Thumbs.db 22 | -------------------------------------------------------------------------------- /src/mtga-tracker-daemon/Asset.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Newtonsoft.Json; 3 | 4 | namespace MTGATrackerDaemon 5 | { 6 | public class Asset 7 | { 8 | [JsonProperty(PropertyName = "name")] 9 | public string Name { get; set; } 10 | 11 | [JsonProperty(PropertyName = "browser_download_url")] 12 | public string BrowserDownloadUrl { get; set; } 13 | } 14 | } -------------------------------------------------------------------------------- /src/mtga-tracker-daemon/DaemonVersion.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Newtonsoft.Json; 3 | 4 | namespace MTGATrackerDaemon 5 | { 6 | public class DaemonVersion 7 | { 8 | [JsonProperty(PropertyName = "tag_name")] 9 | public string TagName { get; set; } 10 | 11 | [JsonProperty(PropertyName = "assets")] 12 | public List Assets { get; set; } 13 | } 14 | } -------------------------------------------------------------------------------- /src/mtga-tracker-daemon/Models/InventoryResponseData.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace MTGATrackerDaemon.Models 4 | { 5 | public class InventoryResponseData 6 | { 7 | [JsonProperty("gems")] 8 | public int Gems { get; set; } 9 | 10 | [JsonProperty("gold")] 11 | public int Gold { get; set; } 12 | 13 | [JsonProperty("elapsedTime")] 14 | public int ElapsedTime { get; set; } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/HackF5.UnitySpy/IMemoryObject.cs: -------------------------------------------------------------------------------- 1 | namespace HackF5.UnitySpy 2 | { 3 | using JetBrains.Annotations; 4 | 5 | /// 6 | /// Represents an object in a process' memory. 7 | /// 8 | [PublicAPI] 9 | public interface IMemoryObject 10 | { 11 | /// 12 | /// Gets the to which the object belongs. 13 | /// 14 | IAssemblyImage Image { get; } 15 | } 16 | } -------------------------------------------------------------------------------- /getVersion.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const path = require("path"); 3 | 4 | const csProjFile = path.join( 5 | __dirname, 6 | "src", 7 | "mtga-tracker-daemon", 8 | "mtga-tracker-daemon.csproj" 9 | ); 10 | 11 | fs.readFile(csProjFile, "utf8", function (err, data) { 12 | if (err) { 13 | return console.log(err); 14 | } 15 | 16 | const versionRegexp = new RegExp("\(.*)\<\/AssemblyVersion\>", "g"); 17 | 18 | const match = versionRegexp.exec(data); 19 | 20 | console.log(match[1]) 21 | }); 22 | -------------------------------------------------------------------------------- /deploy/linux/install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | INSTALL_PATH=/usr/share/mtga-tracker-daemon 3 | if [ -f "$INSTALL_PATH" ]; then 4 | echo "$INSTALL_PATH already exist, daemon cannot be installed." 5 | else 6 | cp -R bin $INSTALL_PATH 7 | cp uninstall.sh $INSTALL_PATH 8 | 9 | SERVICE_NAME=mtga-trackerd.service 10 | 11 | cp $SERVICE_NAME /etc/systemd/system 12 | systemctl daemon-reload 13 | sudo systemctl start $SERVICE_NAME 14 | sudo systemctl enable $SERVICE_NAME 15 | 16 | echo "Daemon installed successfuly" 17 | fi -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test Build 2 | 3 | on: 4 | push: 5 | branches: '**' 6 | 7 | jobs: 8 | build-windows: 9 | runs-on: [ ubuntu-latest ] 10 | strategy: 11 | matrix: 12 | dotnet-version: [ '8.0.x' ] 13 | steps: 14 | - uses: actions/checkout@v2 15 | - name: Setup .NET Core SDK ${{ matrix.dotnet-version }} 16 | uses: actions/setup-dotnet@v1.7.2 17 | with: 18 | dotnet-version: ${{ matrix.dotnet-version }} 19 | - name: Build 20 | run: dotnet build ./src/mtga-tracker-daemon 21 | -------------------------------------------------------------------------------- /src/mtga-tracker-daemon/Models/StatusResponseData.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace MTGATrackerDaemon.Models 4 | { 5 | public class StatusResponseData 6 | { 7 | [JsonProperty("isRunning")] 8 | public bool IsRunning { get; set; } 9 | 10 | [JsonProperty("daemonVersion")] 11 | public string DaemonVersion { get; set; } 12 | 13 | [JsonProperty("updating")] 14 | public bool Updating { get; set; } 15 | 16 | [JsonProperty("processId")] 17 | public int ProcessId { get; set; } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/mtga-tracker-daemon/Models/PlayerIDResponseData.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace MTGATrackerDaemon.Models 4 | { 5 | public class PlayerIDResponseData 6 | { 7 | [JsonProperty("playerId")] 8 | public string PlayerID { get; set; } 9 | 10 | [JsonProperty("displayName")] 11 | public string DisplayName { get; set; } 12 | 13 | [JsonProperty("personaId")] 14 | public string PersonaID { get; set; } 15 | 16 | [JsonProperty("elapsedTime")] 17 | public int ElapsedTime { get; set; } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/mtga-tracker-daemon/Models/CardsResponseData.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace MTGATrackerDaemon.Models 4 | { 5 | public class CardsResponseData 6 | { 7 | [JsonProperty("cards")] 8 | public CardOwnership[] Cards { get; set; } 9 | 10 | [JsonProperty("elapsedTime")] 11 | public int ElapsedTime { get; set; } 12 | } 13 | 14 | public class CardOwnership 15 | { 16 | [JsonProperty("grpId")] 17 | public uint GrpId { get; set; } 18 | 19 | [JsonProperty("owned")] 20 | public int Owned { get; set; } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/mtga-tracker-daemon/StringUtils.cs: -------------------------------------------------------------------------------- 1 | namespace MTGATrackerDaemon 2 | { 3 | public class StringUtils 4 | { 5 | // TODO: Check if we need to escape MTG Arena card titles 6 | public static string JsonEscape(string text) 7 | { 8 | if (text == null) 9 | { 10 | return "null"; 11 | } 12 | 13 | return text.Replace("\\", "\\\\") 14 | .Replace("\"", "\\\"") 15 | .Replace("\r", "\\r") 16 | .Replace("\n", "\\n") 17 | .Replace("\t", "\\t"); 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /src/mtga-tracker-daemon/mtga-tracker-daemon.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net8.0 6 | 1.0.8.1 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/mtga-tracker-daemon/Models/AllCardsResponseData.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace MTGATrackerDaemon.Models 4 | { 5 | public class AllCardsResponseData 6 | { 7 | [JsonProperty("cards")] 8 | public CardInfo[] Cards { get; set; } 9 | 10 | [JsonProperty("elapsedTime")] 11 | public int ElapsedTime { get; set; } 12 | } 13 | 14 | public class CardInfo 15 | { 16 | [JsonProperty("grpId")] 17 | public int GrpId { get; set; } 18 | 19 | [JsonProperty("title")] 20 | public string Title { get; set; } 21 | 22 | [JsonProperty("expansionCode")] 23 | public string ExpansionCode { get; set; } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/HackF5.UnitySpy/ProcessFacade/ModuleInfo.cs: -------------------------------------------------------------------------------- 1 | namespace HackF5.UnitySpy.ProcessFacade 2 | { 3 | using System; 4 | using System.Diagnostics; 5 | 6 | [DebuggerDisplay("{" + nameof(ModuleInfo.ModuleName) + "}")] 7 | public class ModuleInfo 8 | { 9 | public ModuleInfo(string moduleName, IntPtr baseAddress, uint size, string path) 10 | { 11 | this.ModuleName = moduleName; 12 | this.BaseAddress = baseAddress; 13 | this.Size = size; 14 | this.Path = path; 15 | } 16 | 17 | public IntPtr BaseAddress { get; } 18 | 19 | public string ModuleName { get; } 20 | 21 | public string Path { get; } 22 | 23 | public uint Size { get; } 24 | } 25 | } -------------------------------------------------------------------------------- /src/HackF5.UnitySpy/ProcessFacade/ProcessFacadeMacOS.cs: -------------------------------------------------------------------------------- 1 | namespace HackF5.UnitySpy.ProcessFacade 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Diagnostics; 6 | using System.Runtime.InteropServices; 7 | using JetBrains.Annotations; 8 | 9 | /// 10 | /// A MacOS specific facade over a process that provides access to its memory space. 11 | /// 12 | [PublicAPI] 13 | public abstract class ProcessFacadeMacOS : ProcessFacade 14 | { 15 | private readonly Process process; 16 | 17 | public ProcessFacadeMacOS(Process process) 18 | { 19 | this.process = process; 20 | } 21 | 22 | public Process Process => this.process; 23 | } 24 | } -------------------------------------------------------------------------------- /src/HackF5.UnitySpy/Offsets/MachOFormatOffsets.cs: -------------------------------------------------------------------------------- 1 | // ReSharper disable IdentifierTypo 2 | namespace HackF5.UnitySpy.Offsets 3 | { 4 | public static class MachOFormatOffsets 5 | { 6 | // offsets taken from https://opensource.apple.com/source/xnu/xnu-4570.71.2/EXTERNAL_HEADERS/mach-o/loader.h.auto.html 7 | public const int NumberOfCommands = 0x10; 8 | 9 | public const int LoadCommands = 0x20; 10 | 11 | public const int CommandSize = 0x04; 12 | 13 | public const int SymbolTableOffset = 0x08; 14 | 15 | public const int NumberOfSymbols = 0x0c; 16 | 17 | public const int StringTableOffset = 0x10; 18 | 19 | public const int NListValue = 0x08; 20 | 21 | public const int SizeOfNListItem = 0x10; 22 | } 23 | } -------------------------------------------------------------------------------- /src/HackF5.UnitySpy/Offsets/PEFormatOffsets.cs: -------------------------------------------------------------------------------- 1 | // ReSharper disable IdentifierTypo 2 | namespace HackF5.UnitySpy.Offsets 3 | { 4 | public static class PEFormatOffsets 5 | { 6 | // offsets taken from https://docs.microsoft.com/en-us/windows/desktop/Debug/pe-format 7 | public const int Signature = 0x3c; 8 | 9 | // 32 bits 10 | public const int ExportDirectoryIndexPE = 0x78; 11 | 12 | // 64 bits 13 | public const int ExportDirectoryIndexPE32Plus = 0x88; 14 | 15 | public const int NumberOfFunctions = 0x14; 16 | 17 | public const int FunctionAddressArrayIndex = 0x1c; 18 | 19 | public const int FunctionNameArrayIndex = 0x20; 20 | 21 | public const int FunctionEntrySize = 4; 22 | 23 | public static int GetExportDirectoryIndex(bool is64Bits) 24 | { 25 | return is64Bits ? ExportDirectoryIndexPE32Plus : ExportDirectoryIndexPE; 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /src/HackF5.UnitySpy/ProcessFacade/ProcessFacadeLinuxClient.cs: -------------------------------------------------------------------------------- 1 | namespace HackF5.UnitySpy.ProcessFacade 2 | { 3 | using System; 4 | using JetBrains.Annotations; 5 | 6 | /// 7 | /// A Linux specific facade over a process that provides access to its memory space 8 | /// through a server running with /proc/$pid/mem read privileges. 9 | /// 10 | [PublicAPI] 11 | public class ProcessFacadeLinuxClient : ProcessFacadeLinux 12 | { 13 | private readonly ProcessFacadeClient client; 14 | 15 | public ProcessFacadeLinuxClient(int processId) 16 | : base(processId) 17 | { 18 | this.client = new ProcessFacadeClient(processId); 19 | } 20 | 21 | protected override void ReadProcessMemory( 22 | byte[] buffer, 23 | IntPtr processAddress, 24 | int length) 25 | => this.client.ReadProcessMemory(buffer, processAddress, length); 26 | } 27 | } -------------------------------------------------------------------------------- /src/mtga-tracker-daemon/Models/MatchStateResponseData.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace MTGATrackerDaemon.Models 4 | { 5 | public class MatchStateResponseData 6 | { 7 | [JsonProperty("matchId")] 8 | public string MatchId { get; set; } 9 | 10 | [JsonProperty("playerRank")] 11 | public PlayerRank PlayerRank { get; set; } 12 | 13 | [JsonProperty("opponentRank")] 14 | public PlayerRank OpponentRank { get; set; } 15 | 16 | [JsonProperty("elapsedTime")] 17 | public int ElapsedTime { get; set; } 18 | } 19 | 20 | public class PlayerRank 21 | { 22 | [JsonProperty("mythicPercentile")] 23 | public float MythicPercentile { get; set; } 24 | 25 | [JsonProperty("mythicPlacement")] 26 | public int MythicPlacement { get; set; } 27 | 28 | [JsonProperty("class")] 29 | public int Class { get; set; } 30 | 31 | [JsonProperty("tier")] 32 | public int Tier { get; set; } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/HackF5.UnitySpy/IFieldDefinition.cs: -------------------------------------------------------------------------------- 1 | namespace HackF5.UnitySpy 2 | { 3 | using JetBrains.Annotations; 4 | 5 | /// 6 | /// Represents an unmanaged _MonoClassField instance in a Mono process. This object describes a field in a 7 | /// managed class or struct. The .NET equivalent is . 8 | /// See: _MonoImage in https://github.com/Unity-Technologies/mono/blob/unity-master/mono/metadata/class-internals.h. 9 | /// 10 | [PublicAPI] 11 | public interface IFieldDefinition : IMemoryObject 12 | { 13 | /// 14 | /// Gets the name of the field. 15 | /// 16 | string Name { get; } 17 | 18 | /// 19 | /// Gets the of the type in which the field is declared. 20 | /// 21 | ITypeDefinition DeclaringType { get; } 22 | 23 | /// 24 | /// Gets an object that describes type information about field. 25 | /// 26 | ITypeInfo TypeInfo { get; } 27 | } 28 | } -------------------------------------------------------------------------------- /src/HackF5.UnitySpy/HackF5.UnitySpy.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | 1701;1702;CA1724;CA1031 6 | latest 7 | true 8 | ..\HackF5.UnitySpy.snk 9 | AnyCPU 10 | true 11 | 12 | 13 | 14 | true 15 | 16 | 17 | 18 | 19 | x64 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/HackF5.UnitySpy/ProcessFacade/ProcessFacadeLinuxDirect.cs: -------------------------------------------------------------------------------- 1 | namespace HackF5.UnitySpy.ProcessFacade 2 | { 3 | using System; 4 | using System.IO; 5 | using JetBrains.Annotations; 6 | 7 | /// 8 | /// A Linux specific facade over a process that provides access to its memory space 9 | /// that requires /proc/$pid/mem read privileges. 10 | /// 11 | [PublicAPI] 12 | public class ProcessFacadeLinuxDirect : ProcessFacadeLinux 13 | { 14 | private readonly string memFilePath; 15 | 16 | public ProcessFacadeLinuxDirect(int processId, string memFilePath) 17 | : base(processId) 18 | { 19 | this.memFilePath = memFilePath; 20 | } 21 | 22 | protected unsafe override void ReadProcessMemory( 23 | byte[] buffer, 24 | IntPtr processAddress, 25 | int length) 26 | { 27 | using (FileStream memFileStream = new FileStream(this.memFilePath, FileMode.Open)) 28 | { 29 | memFileStream.Seek(processAddress.ToInt64(), 0); 30 | if (length != memFileStream.Read(buffer, 0, length)) 31 | { 32 | throw new Exception("Error reading data from " + this.memFilePath); 33 | } 34 | } 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /src/HackF5.UnitySpy/ProcessFacade/ProcessFacadeMacOSClient.cs: -------------------------------------------------------------------------------- 1 | namespace HackF5.UnitySpy.ProcessFacade 2 | { 3 | using System; 4 | using System.Diagnostics; 5 | using JetBrains.Annotations; 6 | 7 | /// 8 | /// A MacOS specific facade over a process that provides access to its memory space 9 | /// through a server running with root privileges. 10 | /// 11 | [PublicAPI] 12 | public class ProcessFacadeMacOSClient : ProcessFacadeMacOS 13 | { 14 | private readonly ProcessFacadeClient client; 15 | 16 | public ProcessFacadeMacOSClient(Process process) 17 | : base(process) 18 | { 19 | this.client = new ProcessFacadeClient(process.Id); 20 | } 21 | 22 | public override void ReadProcessMemory( 23 | byte[] buffer, 24 | IntPtr processAddress, 25 | bool allowPartialRead = false, 26 | int? size = default) 27 | { 28 | if (buffer == null) 29 | { 30 | throw new ArgumentNullException("the buffer parameter cannot be null"); 31 | } 32 | 33 | this.client.ReadProcessMemory(buffer, processAddress, size ?? buffer.Length); 34 | } 35 | 36 | public override ModuleInfo GetModule(string moduleName) 37 | => this.client.GetModuleInfo(moduleName); 38 | } 39 | } -------------------------------------------------------------------------------- /src/HackF5.UnitySpy/Detail/MemoryObject.cs: -------------------------------------------------------------------------------- 1 | namespace HackF5.UnitySpy.Detail 2 | { 3 | using System; 4 | using HackF5.UnitySpy.ProcessFacade; 5 | 6 | /// 7 | /// The base type for all objects accessed in a process' memory. Every object has an address in memory 8 | /// and all information about that object is accessed via an offset from that address. 9 | /// 10 | public abstract class MemoryObject : IMemoryObject 11 | { 12 | protected MemoryObject(AssemblyImage image, IntPtr address) 13 | { 14 | this.Image = image; 15 | this.Address = address; 16 | } 17 | 18 | IAssemblyImage IMemoryObject.Image => this.Image; 19 | 20 | public virtual AssemblyImage Image { get; } 21 | 22 | public virtual UnityProcessFacade Process => this.Image.Process; 23 | 24 | public IntPtr Address { get; } 25 | 26 | protected int ReadInt32(int offset) => this.Process.ReadInt32(this.Address + offset); 27 | 28 | protected IntPtr ReadPtr(int offset) => this.Process.ReadPtr(this.Address + offset); 29 | 30 | protected string ReadString(int offset) => this.Process.ReadAsciiStringPtr(this.Address + offset); 31 | 32 | protected uint ReadUInt32(int offset) => this.Process.ReadUInt32(this.Address + offset); 33 | 34 | protected byte ReadByte(int offset) => this.Process.ReadByte(this.Address + offset); 35 | } 36 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mtga-tracker-daemon 2 | 3 | An HTTP server for getting game data from MTG Arena. 4 | 5 | Usage is very straightforward; 6 | 7 | `./mtga-tracker-daemon.exe -p 9000` 8 | 9 | ### GET /status 10 | Check if the MTGA process is running or not and whether is updating itself or not, and get its Process ID. 11 | (some apps use this to get other metrics or data like the window position and size) 12 | 13 | Response 14 | ``` 15 | { 16 | isRunning: Boolean, 17 | daemonVersion: String, 18 | updating: Boolean, 19 | processId: Number | -1, 20 | } 21 | ``` 22 | 23 | ### POST /shutdown 24 | Stops the daemon 25 | 26 | Response: 27 | ``` 28 | { 29 | result: String, 30 | } 31 | ``` 32 | 33 | ### POST /checkForUpdates 34 | Tells the daemon to check for updates 35 | 36 | Response: 37 | ``` 38 | { 39 | updatesAvailable: Boolean, 40 | } 41 | ``` 42 | ### GET /cards 43 | Get a list (array) of all cards owned by the current player. 44 | 45 | Response: 46 | ``` 47 | { 48 | cards: [ 49 | { 50 | grpId: Number, 51 | owned: Number, 52 | } 53 | ], 54 | elapsedTime: Number, 55 | } 56 | ``` 57 | 58 | ### GET /playerId 59 | Return the current ID (wizards account ID) 60 | 61 | Response: 62 | ``` 63 | { 64 | playerId: String, 65 | elapsedTime: Number, 66 | } 67 | ``` 68 | 69 | ### GET /inventory 70 | Return the state of the player inventory 71 | 72 | Response: 73 | ``` 74 | { 75 | gems: Number, 76 | gold: Number, 77 | elapsedTime: Number, 78 | } 79 | ``` 80 | 81 | -------------------------------------------------------------------------------- /src/HackF5.UnitySpy/Detail/ManagedObjectInstance.cs: -------------------------------------------------------------------------------- 1 | namespace HackF5.UnitySpy.Detail 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | 6 | public abstract class ManagedObjectInstance : MemoryObject, IManagedObjectInstance 7 | { 8 | private readonly List genericTypeArguments; 9 | 10 | protected ManagedObjectInstance(AssemblyImage image, List genericTypeArguments, IntPtr address) 11 | : base(image, address) 12 | { 13 | this.genericTypeArguments = genericTypeArguments; 14 | } 15 | 16 | ITypeDefinition IManagedObjectInstance.TypeDefinition => this.TypeDefinition; 17 | 18 | public abstract TypeDefinition TypeDefinition { get; } 19 | 20 | public dynamic this[string fieldName] => this.GetValue(fieldName); 21 | 22 | public dynamic this[string fieldName, string typeFullName] => this.GetValue(fieldName, typeFullName); 23 | 24 | public TValue GetValue(string fieldName) => this.GetValue(fieldName, default); 25 | 26 | public TValue GetValue(string fieldName, string typeFullName) 27 | { 28 | var field = this.TypeDefinition.GetField(fieldName, typeFullName) 29 | ?? throw new ArgumentException( 30 | $"No field exists with name {fieldName} in type {typeFullName ?? ""}."); 31 | 32 | return field.GetValue(this.genericTypeArguments, this.Address); 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /src/HackF5.UnitySpy/Detail/ManagedClassInstance.cs: -------------------------------------------------------------------------------- 1 | namespace HackF5.UnitySpy.Detail 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using JetBrains.Annotations; 6 | 7 | /// 8 | /// Represents a class instance in managed memory. 9 | /// Mono and .NET don't necessarily use the same layout scheme, but assuming it is similar this article provides 10 | /// some useful information: 11 | /// https://web.archive.org/web/20080919091745/http://msdn.microsoft.com:80/en-us/magazine/cc163791.aspx. 12 | /// 13 | [PublicAPI] 14 | public class ManagedClassInstance : ManagedObjectInstance 15 | { 16 | private readonly IntPtr definitionAddress; 17 | 18 | private readonly IntPtr vtable; 19 | 20 | public ManagedClassInstance([NotNull] AssemblyImage image, List genericTypeArguments, IntPtr address) 21 | : base(image, genericTypeArguments, address) 22 | { 23 | if (image == null) 24 | { 25 | throw new ArgumentNullException(nameof(image)); 26 | } 27 | 28 | // the address of the class instance points directly back the the classes VTable 29 | this.vtable = this.ReadPtr(0x0); 30 | 31 | // The VTable points to the class definition itself. 32 | this.definitionAddress = image.Process.ReadPtr(this.vtable); 33 | } 34 | 35 | public override TypeDefinition TypeDefinition => this.Image.GetTypeDefinition(this.definitionAddress); 36 | } 37 | } -------------------------------------------------------------------------------- /src/HackF5.UnitySpy/IAssemblyImage.cs: -------------------------------------------------------------------------------- 1 | namespace HackF5.UnitySpy 2 | { 3 | using System.Collections.Generic; 4 | using JetBrains.Annotations; 5 | 6 | /// 7 | /// Represents an unmanaged _MonoImage instance in a Mono process. This object describes a managed assembly. 8 | /// The .NET equivalent is . 9 | /// See: _MonoImage in https://github.com/Unity-Technologies/mono/blob/unity-master/mono/metadata/metadata-internals.h. 10 | /// 11 | [PublicAPI] 12 | public interface IAssemblyImage : IMemoryObject 13 | { 14 | /// 15 | /// Gets the type definitions that are referenced by the assembly. So for example, although 16 | /// is not declared in the assembly, since all types inherit from this type, it is 17 | /// available in this collection. 18 | /// 19 | IEnumerable TypeDefinitions { get; } 20 | 21 | dynamic this[string fullTypeName] { get; } 22 | 23 | /// 24 | /// Gets the with given from the assembly image. 25 | /// 26 | /// The full name of the definition including namespace. For example 'System.Object' 27 | /// not 'object'. 28 | /// 29 | /// The with given from the assembly image, or 30 | /// null if no such definition exists. 31 | /// 32 | ITypeDefinition GetTypeDefinition(string fullName); 33 | } 34 | } -------------------------------------------------------------------------------- /src/HackF5.UnitySpy/Detail/ManagedStructInstance.cs: -------------------------------------------------------------------------------- 1 | namespace HackF5.UnitySpy.Detail 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using JetBrains.Annotations; 6 | 7 | /// 8 | /// This class represents a value type /struct instance in managed memory. 9 | /// Mono and .NET don't necessarily use the same layout scheme, but assuming it is similar this article provides 10 | /// some useful information: 11 | /// https://web.archive.org/web/20080919091745/http://msdn.microsoft.com:80/en-us/magazine/cc163791.aspx. 12 | /// 13 | [PublicAPI] 14 | public class ManagedStructInstance : ManagedObjectInstance 15 | { 16 | public ManagedStructInstance([NotNull] TypeDefinition typeDefinition, List genericTypeArguments, IntPtr address) 17 | : base((typeDefinition ?? throw new ArgumentNullException(nameof(typeDefinition))).Image, genericTypeArguments, address) 18 | { 19 | // value type pointers contain no type information as a significant performance optimization. in memory 20 | // a value type is simply a contiguous sequence of bytes and it is up to the runtime to know how to 21 | // interpret those bytes. if you take the example of a integer, then it makes sense why this is 22 | // as if an integer needed an extra pointer that pointed back to the System.Int32 class then the size 23 | // of each one would be doubled. presumably interoperability with native assemblies would also be 24 | // completely scuppered. 25 | this.TypeDefinition = typeDefinition; 26 | } 27 | 28 | public override TypeDefinition TypeDefinition { get; } 29 | } 30 | } -------------------------------------------------------------------------------- /src/HackF5.UnitySpy/ITypeInfo.cs: -------------------------------------------------------------------------------- 1 | namespace HackF5.UnitySpy 2 | { 3 | using HackF5.UnitySpy.Detail; 4 | using JetBrains.Annotations; 5 | 6 | /// 7 | /// Represents an unmanaged _MonoType instance in a Mono process. This object describes type information. 8 | /// There is no direct .NET equivalent, but probably comes under the umbrella of . 9 | /// See: _MonoType in https://github.com/Unity-Technologies/mono/blob/unity-master/mono/metadata/metadata-internals.h. 10 | /// 11 | [PublicAPI] 12 | public interface ITypeInfo 13 | { 14 | /// 15 | /// Gets a value indicating whether the entity the info refers to is static; i.e. a static field or a static 16 | /// class. 17 | /// 18 | bool IsStatic { get; } 19 | 20 | /// 21 | /// Gets a value indicating whether the field the info refers to is a constant. 22 | /// 23 | bool IsConstant { get; } 24 | 25 | /// 26 | /// Gets a value that describes the type of the entity. 27 | /// 28 | TypeCode TypeCode { get; } 29 | 30 | /// 31 | /// Tries to get the that this type info refers to. If the return value is 32 | /// false then refer to information in the . 33 | /// 34 | /// 35 | /// The that this type info refers to. 36 | /// 37 | /// 38 | /// A value indicating success. 39 | /// 40 | bool TryGetTypeDefinition(out ITypeDefinition typeDefinition); 41 | } 42 | } -------------------------------------------------------------------------------- /src/HackF5.UnitySpy/Util/ConversionUtils.cs: -------------------------------------------------------------------------------- 1 | namespace HackF5.UnitySpy.Util 2 | { 3 | using System; 4 | using System.Linq; 5 | using System.Text; 6 | 7 | public static class ConversionUtils 8 | { 9 | public static string ToAsciiString(this byte[] buffer, int start = 0) 10 | { 11 | var length = buffer.Skip(start).TakeWhile(b => b != 0).Count(); 12 | return Encoding.ASCII.GetString(buffer, start, length); 13 | } 14 | 15 | public static int ToInt32(this byte[] buffer, int start = 0) => BitConverter.ToInt32(buffer, start); 16 | 17 | public static uint ToUInt32(this byte[] buffer, int start = 0) => BitConverter.ToUInt32(buffer, start); 18 | 19 | public static ulong ToUInt64(this byte[] buffer, int start = 0) => BitConverter.ToUInt64(buffer, start); 20 | 21 | public static char ToChar(this byte[] buffer) => BitConverter.ToChar(buffer, 0); 22 | 23 | public static ushort ToUInt16(this byte[] buffer) => BitConverter.ToUInt16(buffer, 0); 24 | 25 | public static short ToInt16(this byte[] buffer) => BitConverter.ToInt16(buffer, 0); 26 | 27 | public static ulong ToUInt64(this byte[] buffer) => BitConverter.ToUInt64(buffer, 0); 28 | 29 | public static long ToInt64(this byte[] buffer) => BitConverter.ToInt64(buffer, 0); 30 | 31 | public static float ToSingle(this byte[] buffer) => BitConverter.ToSingle(buffer, 0); 32 | 33 | public static double ToDouble(this byte[] buffer) => BitConverter.ToDouble(buffer, 0); 34 | 35 | public static byte ToByte(this byte[] buffer) 36 | { 37 | if (buffer == null || buffer.Length == 0) 38 | { 39 | throw new ArgumentNullException("the buffer parameter cannot be null or empty"); 40 | } 41 | 42 | return buffer[0]; 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /src/HackF5.UnitySpy/Detail/TypeInfo.cs: -------------------------------------------------------------------------------- 1 | namespace HackF5.UnitySpy.Detail 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using JetBrains.Annotations; 6 | 7 | /// 8 | /// Represents an unmanaged _MonoType instance in a Mono process. This object describes type information. 9 | /// There is no direct .NET equivalent, but probably comes under the umbrella of . 10 | /// See: _MonoType in https://github.com/Unity-Technologies/mono/blob/unity-master/mono/metadata/metadata-internals.h. 11 | /// 12 | [PublicAPI] 13 | public class TypeInfo : MemoryObject, ITypeInfo 14 | { 15 | public TypeInfo(AssemblyImage image, IntPtr address) 16 | : base(image, address) 17 | { 18 | this.Data = this.ReadPtr(0x0); 19 | this.Attrs = this.ReadUInt32(this.Process.SizeOfPtr); 20 | } 21 | 22 | public uint Attrs { get; } 23 | 24 | public IntPtr Data { get; } 25 | 26 | public bool IsStatic => (this.Attrs & 0x10) == 0x10; 27 | 28 | public bool IsConstant => (this.Attrs & 0x40) == 0x40; 29 | 30 | public TypeCode TypeCode => (TypeCode)(0xff & (this.Attrs >> 16)); 31 | 32 | public bool TryGetTypeDefinition(out ITypeDefinition typeDefinition) 33 | { 34 | switch (this.TypeCode) 35 | { 36 | case TypeCode.CLASS: 37 | case TypeCode.SZARRAY: 38 | case TypeCode.GENERICINST: 39 | case TypeCode.VALUETYPE: 40 | typeDefinition = this.Image.GetTypeDefinition(this.Process.ReadPtr(this.Data)); 41 | return true; 42 | default: 43 | typeDefinition = null; 44 | return false; 45 | } 46 | } 47 | 48 | public object GetValue(List genericTypeArguments, IntPtr address) 49 | => this.Process.ReadManaged(this, genericTypeArguments, address); 50 | } 51 | } -------------------------------------------------------------------------------- /mtga-tracker-daemon.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.5.002.0 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{BDE24B67-D899-464E-A6A5-5A5AAD0B864C}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HackF5.UnitySpy", "src\HackF5.UnitySpy\HackF5.UnitySpy.csproj", "{966393CD-AA1A-4A11-9D66-93925330220E}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "mtga-tracker-daemon", "src\mtga-tracker-daemon\mtga-tracker-daemon.csproj", "{B5299BF3-C342-4FC6-AADC-B9133D01796A}" 11 | EndProject 12 | Global 13 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 14 | Debug|Any CPU = Debug|Any CPU 15 | Release|Any CPU = Release|Any CPU 16 | EndGlobalSection 17 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 18 | {966393CD-AA1A-4A11-9D66-93925330220E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 19 | {966393CD-AA1A-4A11-9D66-93925330220E}.Debug|Any CPU.Build.0 = Debug|Any CPU 20 | {966393CD-AA1A-4A11-9D66-93925330220E}.Release|Any CPU.ActiveCfg = Release|Any CPU 21 | {966393CD-AA1A-4A11-9D66-93925330220E}.Release|Any CPU.Build.0 = Release|Any CPU 22 | {B5299BF3-C342-4FC6-AADC-B9133D01796A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 23 | {B5299BF3-C342-4FC6-AADC-B9133D01796A}.Debug|Any CPU.Build.0 = Debug|Any CPU 24 | {B5299BF3-C342-4FC6-AADC-B9133D01796A}.Release|Any CPU.ActiveCfg = Release|Any CPU 25 | {B5299BF3-C342-4FC6-AADC-B9133D01796A}.Release|Any CPU.Build.0 = Release|Any CPU 26 | EndGlobalSection 27 | GlobalSection(SolutionProperties) = preSolution 28 | HideSolutionNode = FALSE 29 | EndGlobalSection 30 | GlobalSection(NestedProjects) = preSolution 31 | {966393CD-AA1A-4A11-9D66-93925330220E} = {BDE24B67-D899-464E-A6A5-5A5AAD0B864C} 32 | {B5299BF3-C342-4FC6-AADC-B9133D01796A} = {BDE24B67-D899-464E-A6A5-5A5AAD0B864C} 33 | EndGlobalSection 34 | GlobalSection(ExtensibilityGlobals) = postSolution 35 | SolutionGuid = {9AE3CB59-DF9B-4D70-81CE-60406422789F} 36 | EndGlobalSection 37 | EndGlobal 38 | -------------------------------------------------------------------------------- /src/mtga-tracker-daemon/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | 4 | namespace MTGATrackerDaemon 5 | { 6 | 7 | class Program 8 | { 9 | public const string BaseUrl = "http://localhost:"; 10 | 11 | public const int DefaultPort = 6842; 12 | 13 | static void Main(string[] args) 14 | { 15 | int port = DefaultPort; 16 | 17 | int i = 0; 18 | while (i < args.Length) 19 | { 20 | switch (args[i]) 21 | { 22 | case "-p": 23 | if (++i < args.Length) 24 | { 25 | try 26 | { 27 | port = int.Parse(args[i]); 28 | } 29 | catch (Exception) 30 | { 31 | Console.WriteLine("Port number format incorrect"); 32 | return; 33 | } 34 | } 35 | else 36 | { 37 | DisplayUsageMessages(); 38 | return; 39 | } 40 | break; 41 | } 42 | i++; 43 | } 44 | 45 | HttpServer server = new HttpServer(); 46 | server.Start(BaseUrl + port + "/"); 47 | } 48 | 49 | private static void DisplayUsageMessages() 50 | { 51 | string usageMessage; 52 | string exampleMessage; 53 | 54 | if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) 55 | { 56 | usageMessage = "Usage: ./mtga-tracker-daemon.exe [-p PORT]"; 57 | exampleMessage = "Example: ./mtga-tracker-daemon.exe -p 9000"; 58 | } 59 | else 60 | { 61 | usageMessage = "Usage: mtga-tracker-daemon [-p PORT]"; 62 | exampleMessage = "Example: mtga-tracker-daemon -p 9000"; 63 | } 64 | 65 | Console.WriteLine(usageMessage); 66 | Console.WriteLine(exampleMessage); 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/HackF5.UnitySpy/IManagedObjectInstance.cs: -------------------------------------------------------------------------------- 1 | namespace HackF5.UnitySpy 2 | { 3 | using JetBrains.Annotations; 4 | 5 | /// 6 | /// Represents an object instance in managed memory. 7 | /// 8 | [PublicAPI] 9 | public interface IManagedObjectInstance : IMemoryObject 10 | { 11 | /// 12 | /// Gets the that describes the type of this instance. 13 | /// 14 | ITypeDefinition TypeDefinition { get; } 15 | 16 | dynamic this[string fieldName] { get; } 17 | 18 | dynamic this[string fieldName, string typeFullName] { get; } 19 | 20 | /// 21 | /// Gets the value of the field in the instance with the given . 22 | /// 23 | /// 24 | /// The type of the value to get. If unsure of the type you can always use . 25 | /// 26 | /// 27 | /// The name of the field. 28 | /// 29 | /// 30 | /// The value of the field in the instance with the given . 31 | /// 32 | TValue GetValue(string fieldName); 33 | 34 | /// 35 | /// Gets the value of the field in the instance with the given that is declared 36 | /// in type with given . 37 | /// 38 | /// 39 | /// The type of the value to get. If unsure of the type you can always use . 40 | /// 41 | /// 42 | /// The name of the field. 43 | /// 44 | /// 45 | /// The name of the type in which the field is declared. In most cases this can be left null, however 46 | /// to access a private field in a subclass which has the same name as a field declared higher up in the 47 | /// hierarchy it is necessary to provide the full name of the declaring type. 48 | /// 49 | /// 50 | /// The value of the field in the instance with the given . 51 | /// 52 | TValue GetValue(string fieldName, string typeFullName); 53 | } 54 | } -------------------------------------------------------------------------------- /src/HackF5.UnitySpy/ProcessFacade/ProcessFacadeLinuxPTrace.cs: -------------------------------------------------------------------------------- 1 | namespace HackF5.UnitySpy.ProcessFacade 2 | { 3 | using System; 4 | using System.Runtime.InteropServices; 5 | using JetBrains.Annotations; 6 | 7 | /// 8 | /// A Linux specific facade over a process that provides access to its memory space through ptrace. 9 | /// 10 | [PublicAPI] 11 | public class ProcessFacadeLinuxPTrace : ProcessFacadeLinux 12 | { 13 | private readonly int processId; 14 | 15 | public ProcessFacadeLinuxPTrace(int processId) 16 | : base(processId) 17 | { 18 | this.processId = processId; 19 | } 20 | 21 | public int ProcessId => this.processId; 22 | 23 | protected unsafe override void ReadProcessMemory( 24 | byte[] buffer, 25 | IntPtr processAddress, 26 | int length) 27 | { 28 | fixed (byte* bytePtr = buffer) 29 | { 30 | var ptr = (IntPtr)bytePtr; 31 | var localIo = new Iovec 32 | { 33 | IovBase = ptr.ToPointer(), 34 | IovLen = length, 35 | }; 36 | var remoteIo = new Iovec 37 | { 38 | IovBase = processAddress.ToPointer(), 39 | IovLen = length, 40 | }; 41 | 42 | var res = ProcessVmReadV(this.ProcessId, &localIo, 1, &remoteIo, 1, 0); 43 | if (res != -1) 44 | { 45 | // Array.Copy(*(byte[]*)ptr, 0, buffer, 0, length); 46 | Marshal.Copy(ptr, buffer, 0, length); 47 | } 48 | else 49 | { 50 | throw new Exception("Error while trying to read memory through from process_vm_readv. Check errno."); 51 | } 52 | } 53 | } 54 | 55 | [DllImport("libc", EntryPoint = "process_vm_readv")] 56 | private static extern unsafe int ProcessVmReadV( 57 | int pid, 58 | Iovec* local_iov, 59 | ulong liovcnt, 60 | Iovec* remote_iov, 61 | ulong riovcnt, 62 | ulong flags); 63 | 64 | [StructLayout(LayoutKind.Sequential)] 65 | private unsafe struct Iovec 66 | { 67 | public void* IovBase; 68 | public int IovLen; 69 | } 70 | } 71 | } -------------------------------------------------------------------------------- /src/HackF5.UnitySpy/ProcessFacade/UnityProcessFacade.cs: -------------------------------------------------------------------------------- 1 | namespace HackF5.UnitySpy.ProcessFacade 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using HackF5.UnitySpy.Detail; 6 | using HackF5.UnitySpy.Offsets; 7 | using JetBrains.Annotations; 8 | 9 | /// 10 | /// A facade over an unity process that provides access to its memory space. 11 | /// 12 | [PublicAPI] 13 | public class UnityProcessFacade 14 | { 15 | private readonly ProcessFacade process; 16 | 17 | private readonly MonoLibraryOffsets monoLibraryOffsets; 18 | 19 | public UnityProcessFacade(ProcessFacade process, MonoLibraryOffsets monoLibraryOffsets) 20 | { 21 | this.process = process; 22 | this.monoLibraryOffsets = monoLibraryOffsets; 23 | 24 | if (monoLibraryOffsets != null) 25 | { 26 | this.process.Is64Bits = monoLibraryOffsets.Is64Bits; 27 | } 28 | } 29 | 30 | public MonoLibraryOffsets MonoLibraryOffsets => this.monoLibraryOffsets; 31 | 32 | public bool Is64Bits => this.process.Is64Bits; 33 | 34 | public int SizeOfPtr => this.process.SizeOfPtr; 35 | 36 | public ProcessFacade Process => this.process; 37 | 38 | public string ReadAsciiString(IntPtr address, int maxSize = 1024) => 39 | this.process.ReadAsciiString(address, maxSize); 40 | 41 | public string ReadAsciiStringPtr(IntPtr address, int maxSize = 1024) => 42 | this.ReadAsciiString(this.ReadPtr(address), maxSize); 43 | 44 | public int ReadInt32(IntPtr address) => this.process.ReadInt32(address); 45 | 46 | public long ReadInt64(IntPtr address) => this.process.ReadInt64(address); 47 | 48 | public object ReadManaged([NotNull] TypeInfo type, List genericTypeArguments, IntPtr address) 49 | => this.process.ReadManaged(type, genericTypeArguments, address); 50 | 51 | public IntPtr ReadPtr(IntPtr address) => this.process.ReadPtr(address); 52 | 53 | public uint ReadUInt32(IntPtr address) => this.process.ReadUInt32(address); 54 | 55 | public ulong ReadUInt64(IntPtr address) => this.process.ReadUInt64(address); 56 | 57 | public byte ReadByte(IntPtr address) => this.process.ReadByte(address); 58 | 59 | public byte[] ReadModule([NotNull] ModuleInfo moduleInfo) => this.process.ReadModule(moduleInfo); 60 | 61 | public ModuleInfo GetMonoModule() => this.process.GetModule(this.monoLibraryOffsets.MonoLibrary); 62 | } 63 | } -------------------------------------------------------------------------------- /src/HackF5.UnitySpy/Detail/TypeCode.cs: -------------------------------------------------------------------------------- 1 | // 2 | namespace HackF5.UnitySpy.Detail 3 | { 4 | using System.ComponentModel; 5 | 6 | /// 7 | /// Represents the type of an object in managed memory. 8 | /// See: MonoTypeEnum in https://github.com/Unity-Technologies/mono/blob/unity-master/mono/metadata/blob.h. 9 | /// 10 | public enum TypeCode 11 | { 12 | END = 0x00, /* End of List */ 13 | VOID = 0x01, 14 | 15 | [Description("bool")] 16 | BOOLEAN = 0x02, 17 | 18 | [Description("char")] 19 | CHAR = 0x03, 20 | 21 | [Description("byte")] 22 | I1 = 0x04, 23 | 24 | [Description("sbyte")] 25 | U1 = 0x05, 26 | 27 | [Description("short")] 28 | I2 = 0x06, 29 | 30 | [Description("ushort")] 31 | U2 = 0x07, 32 | 33 | [Description("int")] 34 | I4 = 0x08, 35 | 36 | [Description("uint")] 37 | U4 = 0x09, 38 | 39 | [Description("long")] 40 | I8 = 0x0a, 41 | 42 | [Description("ulong")] 43 | U8 = 0x0b, 44 | 45 | [Description("float")] 46 | R4 = 0x0c, 47 | 48 | [Description("double")] 49 | R8 = 0x0d, 50 | 51 | [Description("string")] 52 | STRING = 0x0e, 53 | 54 | PTR = 0x0f, /* arg: token */ 55 | BYREF = 0x10, /* arg: token */ 56 | VALUETYPE = 0x11, /* arg: token */ 57 | CLASS = 0x12, /* arg: token */ 58 | VAR = 0x13, /* number */ 59 | ARRAY = 0x14, /* type, rank, boundsCount, bound1, loCount, lo1 */ 60 | GENERICINST = 0x15, /* \x{2026} */ 61 | TYPEDBYREF = 0x16, 62 | 63 | [Description("int")] 64 | I = 0x18, 65 | 66 | [Description("uint")] 67 | U = 0x19, 68 | 69 | FNPTR = 0x1b, /* arg: full method signature */ 70 | OBJECT = 0x1c, 71 | SZARRAY = 0x1d, /* 0-based one-dim-array */ 72 | MVAR = 0x1e, /* number */ 73 | CMOD_REQD = 0x1f, /* arg: typedef or typeref token */ 74 | CMOD_OPT = 0x20, /* optional arg: typedef or typref token */ 75 | INTERNAL = 0x21, /* CLR internal type */ 76 | MODIFIER = 0x40, /* Or with the following types */ 77 | SENTINEL = 0x41, /* Sentinel for varargs method signature */ 78 | PINNED = 0x45, /* Local var that points to pinned object */ 79 | ENUM = 0x55 /* an enumeration */ 80 | } 81 | } -------------------------------------------------------------------------------- /src/HackF5.UnitySpy/Offsets/UnityVersion.cs: -------------------------------------------------------------------------------- 1 | namespace HackF5.UnitySpy.Offsets 2 | { 3 | using System; 4 | using System.Linq; 5 | 6 | public struct UnityVersion 7 | { 8 | public static readonly UnityVersion Version2018_4_10 = new UnityVersion(2018, 4, 10); 9 | public static readonly UnityVersion Version2019_4_5 = new UnityVersion(2019, 4, 5); 10 | public static readonly UnityVersion Version2020_3_13 = new UnityVersion(2020, 3, 13); 11 | public static readonly UnityVersion Version2021_3_14 = new UnityVersion(2021, 3, 14); 12 | public static readonly UnityVersion Version2022_3_42 = new UnityVersion(2022, 3, 42); 13 | 14 | public UnityVersion(int year, int versionWithinYear, int subversionWithinYear) 15 | { 16 | this.Year = year; 17 | this.VersionWithinYear = versionWithinYear; 18 | this.SubversionWithinYear = subversionWithinYear; 19 | } 20 | 21 | public int Year { get; } 22 | 23 | public int VersionWithinYear { get; } 24 | 25 | public int SubversionWithinYear { get; } 26 | 27 | public static bool operator ==(UnityVersion a, UnityVersion b) => a.Equals(b); 28 | 29 | public static bool operator !=(UnityVersion a, UnityVersion b) => !(a == b); 30 | 31 | public static UnityVersion Parse(string version) 32 | { 33 | if (version == null) 34 | { 35 | throw new ArgumentNullException("version paramenter cannot be null"); 36 | } 37 | 38 | string[] versionSplit = version.Split('.'); 39 | int subversionWithinYear = int.Parse(new string(versionSplit[2].TakeWhile(char.IsDigit).ToArray())); 40 | return new UnityVersion(int.Parse(versionSplit[0]), int.Parse(versionSplit[1]), subversionWithinYear); 41 | } 42 | 43 | public override bool Equals(object obj) 44 | { 45 | if (obj is UnityVersion other) 46 | { 47 | return other.Year == this.Year && 48 | other.VersionWithinYear == this.VersionWithinYear && 49 | other.SubversionWithinYear == this.SubversionWithinYear; 50 | } 51 | else 52 | { 53 | return false; 54 | } 55 | } 56 | 57 | public override int GetHashCode() 58 | { 59 | int hash = 17; 60 | hash = (hash * 27) + this.Year.GetHashCode(); 61 | hash = (hash * 23) + this.VersionWithinYear.GetHashCode(); 62 | hash = (hash * 13) + this.SubversionWithinYear.GetHashCode(); 63 | return hash; 64 | } 65 | 66 | public override string ToString() 67 | { 68 | return this.Year + "." + this.VersionWithinYear + "." + this.SubversionWithinYear; 69 | } 70 | } 71 | } -------------------------------------------------------------------------------- /src/HackF5.UnitySpy/Util/ByteArrayPool.cs: -------------------------------------------------------------------------------- 1 | // SEE: http://source.roslyn.codeplex.com/#Microsoft.CodeAnalysis.Workspaces/ObjectPool%25601.cs 2 | 3 | namespace HackF5.UnitySpy.Util 4 | { 5 | using System; 6 | using System.Threading; 7 | using JetBrains.Annotations; 8 | 9 | public class ByteArrayPool 10 | { 11 | private const int BufferSize = 0x10; 12 | 13 | private const int MaxBufferCount = 0x10; 14 | 15 | private static readonly byte[] Empty = new byte[ByteArrayPool.BufferSize]; 16 | 17 | private readonly Element[] items; 18 | 19 | private byte[] firstItem; 20 | 21 | private ByteArrayPool() 22 | { 23 | this.items = new Element[ByteArrayPool.MaxBufferCount]; 24 | } 25 | 26 | public static ByteArrayPool Instance { get; } = new ByteArrayPool(); 27 | 28 | public byte[] Rent(int size) 29 | { 30 | if (size > ByteArrayPool.BufferSize) 31 | { 32 | return new byte[size]; 33 | } 34 | 35 | var item = this.firstItem; 36 | if ((item == null) || (item != Interlocked.CompareExchange(ref this.firstItem, null, item))) 37 | { 38 | var itemsCopy = this.items; 39 | for (var i = 0; i < itemsCopy.Length; i++) 40 | { 41 | // Note that the initial read is optimistically not synchronized. That is intentional. 42 | // We will interlock only when we have a candidate. in a worst case we may miss some 43 | // recently returned objects. Not a big deal. 44 | item = itemsCopy[i].Value; 45 | if (item != null) 46 | { 47 | if (item == Interlocked.CompareExchange(ref itemsCopy[i].Value, null, item)) 48 | { 49 | break; 50 | } 51 | } 52 | } 53 | } 54 | 55 | return item ?? new byte[ByteArrayPool.BufferSize]; 56 | } 57 | 58 | public void Return([NotNull] byte[] buffer) 59 | { 60 | if (buffer == null) 61 | { 62 | throw new ArgumentNullException(nameof(buffer)); 63 | } 64 | 65 | if (buffer.Length > ByteArrayPool.BufferSize) 66 | { 67 | return; 68 | } 69 | 70 | // optimistically clear buffer. 71 | Buffer.BlockCopy(ByteArrayPool.Empty, 0, buffer, 0, ByteArrayPool.BufferSize); 72 | 73 | // Intentionally not using interlocked here. 74 | // In a worst case scenario two objects may be stored into same slot. 75 | // It is very unlikely to happen and will only mean that one of the objects will get collected. 76 | if (this.firstItem == null) 77 | { 78 | this.firstItem = buffer; 79 | } 80 | else 81 | { 82 | var itemsCopy = this.items; 83 | for (var i = 0; i < itemsCopy.Length; i++) 84 | { 85 | if (itemsCopy[i].Value == null) 86 | { 87 | itemsCopy[i].Value = buffer; 88 | break; 89 | } 90 | } 91 | } 92 | } 93 | 94 | private struct Element 95 | { 96 | internal byte[] Value; 97 | } 98 | } 99 | } -------------------------------------------------------------------------------- /src/HackF5.UnitySpy/ITypeDefinition.cs: -------------------------------------------------------------------------------- 1 | namespace HackF5.UnitySpy 2 | { 3 | using System.Collections.Generic; 4 | using JetBrains.Annotations; 5 | 6 | /// 7 | /// Represents an unmanaged _MonoClass instance in a Mono process. This object describes the type of a class or 8 | /// struct. The .NET equivalent is . 9 | /// See: _MonoImage in https://github.com/Unity-Technologies/mono/blob/unity-master/mono/metadata/class-internals.h. 10 | /// 11 | [PublicAPI] 12 | public interface ITypeDefinition : IMemoryObject 13 | { 14 | /// 15 | /// Gets the collection of the in the type and all of its base types. 16 | /// 17 | IReadOnlyList Fields { get; } 18 | 19 | /// 20 | /// Gets the full name of the type, for example 'System.Object'. 21 | /// 22 | string FullName { get; } 23 | 24 | /// 25 | /// Gets a value indicating whether the type derives from . 26 | /// 27 | bool IsEnum { get; } 28 | 29 | /// 30 | /// Gets a value indicating whether the type derives from . 31 | /// 32 | bool IsValueType { get; } 33 | 34 | /// 35 | /// Gets the name of the type. For example for 'System.Object' this value would be 'Object'. 36 | /// 37 | string Name { get; } 38 | 39 | /// 40 | /// Gets the namespace of the type. For example for 'System.Object' this value would be 'System'. 41 | /// 42 | string NamespaceName { get; } 43 | 44 | /// 45 | /// Gets an object that describes further information about the type. 46 | /// 47 | ITypeInfo TypeInfo { get; } 48 | 49 | dynamic this[string fieldName] { get; } 50 | 51 | /// 52 | /// Gets the declared in the type with the given . 53 | /// 54 | /// 55 | /// The name of the field. 56 | /// 57 | /// 58 | /// The name of the type in which the field is declared. In most cases this can be left null, however 59 | /// to access a private field in a subclass which has the same name as a field declared higher up in the 60 | /// hierarchy it is necessary to provide the full name of the declaring type. 61 | /// 62 | /// 63 | /// The declared in the type with the given . 64 | /// 65 | IFieldDefinition GetField(string fieldName, string typeFullName = default); 66 | 67 | /// 68 | /// Gets the value of a static field declared in the type. 69 | /// 70 | /// 71 | /// The type of the value to get. If unsure of the type you can always use . 72 | /// 73 | /// 74 | /// The name of the static field. 75 | /// 76 | /// 77 | /// The value of the static field. 78 | /// 79 | TValue GetStaticValue(string fieldName); 80 | } 81 | } -------------------------------------------------------------------------------- /src/HackF5.UnitySpy/ProcessFacade/ProcessFacadeMacOSDirect.cs: -------------------------------------------------------------------------------- 1 | namespace HackF5.UnitySpy.ProcessFacade 2 | { 3 | using System; 4 | using System.Diagnostics; 5 | using System.Runtime.InteropServices; 6 | using JetBrains.Annotations; 7 | 8 | /// 9 | /// A MacOS specific facade over a process that provides access to its memory space. 10 | /// 11 | [PublicAPI] 12 | public class ProcessFacadeMacOSDirect : ProcessFacadeMacOS 13 | { 14 | public ProcessFacadeMacOSDirect(Process process) 15 | : base(process) 16 | { 17 | } 18 | 19 | public override void ReadProcessMemory( 20 | byte[] buffer, 21 | IntPtr processAddress, 22 | bool allowPartialRead = false, 23 | int? size = default) 24 | { 25 | if (buffer == null) 26 | { 27 | throw new ArgumentNullException("the buffer parameter cannot be null"); 28 | } 29 | 30 | var bufferHandle = GCHandle.Alloc(buffer, GCHandleType.Pinned); 31 | 32 | try 33 | { 34 | var bufferPointer = Marshal.UnsafeAddrOfPinnedArrayElement(buffer, 0); 35 | if (ProcessFacadeMacOSDirect.ReadProcessMemory( 36 | this.Process.Id, 37 | processAddress, 38 | bufferPointer, 39 | size ?? buffer.Length) != 0) 40 | { 41 | var error = Marshal.GetLastWin32Error(); 42 | if ((error == 299) && allowPartialRead) 43 | { 44 | return; 45 | } 46 | 47 | throw new InvalidOperationException($"Could not read memory at address {processAddress.ToString("X")}. Error code: {error}"); 48 | } 49 | } 50 | finally 51 | { 52 | bufferHandle.Free(); 53 | } 54 | } 55 | 56 | public override ModuleInfo GetModule(string moduleName) 57 | { 58 | if (!this.Is64Bits) 59 | { 60 | throw new NotSupportedException("MacOS for 32 binaries is not supported currently"); 61 | } 62 | 63 | IntPtr baseAddress; 64 | uint size = 0; 65 | IntPtr buffer = Marshal.AllocCoTaskMem(2048 + 1); 66 | string path; 67 | try 68 | { 69 | Marshal.WriteByte(buffer, 2048, 0); // Ensure there will be a null byte after call 70 | baseAddress = GetModuleInfo(this.Process.Id, moduleName, ref size, buffer); 71 | path = Marshal.PtrToStringAnsi(buffer); 72 | } 73 | finally 74 | { 75 | Marshal.FreeCoTaskMem(buffer); 76 | } 77 | 78 | return new ModuleInfo(moduleName, baseAddress, size, path); 79 | } 80 | 81 | [DllImport("macos.dylib", EntryPoint = "read_process_memory_to_buffer", SetLastError = true)] 82 | private static extern int ReadProcessMemory( 83 | int processId, 84 | IntPtr lpBaseAddress, 85 | IntPtr lpBuffer, 86 | int nSize); 87 | 88 | [DllImport("macos.dylib", EntryPoint = "get_module_info", SetLastError = true)] 89 | private static extern IntPtr GetModuleInfo( 90 | int processId, 91 | string moduleName, 92 | ref uint nSize, 93 | IntPtr path); 94 | } 95 | } -------------------------------------------------------------------------------- /src/HackF5.UnitySpy/ProcessFacade/ProcessFacadeLinuxDump.cs: -------------------------------------------------------------------------------- 1 | namespace HackF5.UnitySpy.ProcessFacade 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using System.IO; 6 | using JetBrains.Annotations; 7 | 8 | /// 9 | /// A Linux specific facade over a process that provides access to its memory space 10 | /// that requires /proc/$pid/mem read privileges. 11 | /// 12 | [PublicAPI] 13 | public class ProcessFacadeLinuxDump : ProcessFacadeLinux 14 | { 15 | private readonly List dumpFiles; 16 | 17 | public ProcessFacadeLinuxDump(string mapsFilePath, string dumpsPath) 18 | : base(mapsFilePath) 19 | { 20 | string[] dumpFilePaths = Directory.GetFiles(dumpsPath); 21 | this.dumpFiles = new List(dumpFilePaths.Length); 22 | string[] splitFileName; 23 | 24 | foreach (var filePath in dumpFilePaths) 25 | { 26 | splitFileName = new FileInfo(filePath).Name.Split('-'); 27 | if (splitFileName.Length == 2) 28 | { 29 | this.dumpFiles.Add(new MemoryMapping(splitFileName[0], splitFileName[1], filePath, false)); 30 | } 31 | } 32 | } 33 | 34 | protected unsafe override void ReadProcessMemory( 35 | byte[] buffer, 36 | IntPtr processAddress, 37 | int length) 38 | { 39 | this.ReadProcessMemory(buffer, processAddress, 0, length); 40 | } 41 | 42 | private unsafe void ReadProcessMemory( 43 | byte[] buffer, 44 | IntPtr processAddress, 45 | int bufferIndex, 46 | int length) 47 | { 48 | if (buffer == null) 49 | { 50 | throw new ArgumentNullException("the buffer parameter cannot be null"); 51 | } 52 | 53 | lock (this.dumpFiles) 54 | { 55 | foreach (MemoryMapping dumpFile in this.dumpFiles) 56 | { 57 | if (dumpFile.Contains(processAddress)) 58 | { 59 | using (FileStream memFileStream = new FileStream(dumpFile.ModuleName, FileMode.Open)) 60 | { 61 | long fileOffset = processAddress.ToInt64() - dumpFile.StartAddress.ToInt64(); 62 | memFileStream.Seek(fileOffset, 0); 63 | 64 | if (fileOffset + length < dumpFile.Size) 65 | { 66 | if (length != memFileStream.Read(buffer, bufferIndex, length)) 67 | { 68 | throw new Exception("Error reading data from " + dumpFile.ModuleName); 69 | } 70 | } 71 | else 72 | { 73 | int bytesToReadInCurrentFile = Convert.ToInt32(dumpFile.Size - fileOffset); 74 | if (bytesToReadInCurrentFile != memFileStream.Read(buffer, bufferIndex, bytesToReadInCurrentFile)) 75 | { 76 | throw new Exception("Error reading data from " + dumpFile.ModuleName); 77 | } 78 | 79 | int newBufferIndex = bufferIndex + bytesToReadInCurrentFile; 80 | int newLength = length - bytesToReadInCurrentFile; 81 | this.ReadProcessMemory(buffer, dumpFile.EndAddress, newBufferIndex, newLength); 82 | } 83 | } 84 | 85 | return; 86 | } 87 | } 88 | } 89 | } 90 | } 91 | } -------------------------------------------------------------------------------- /src/HackF5.UnitySpy/Detail/FieldDefinition.cs: -------------------------------------------------------------------------------- 1 | namespace HackF5.UnitySpy.Detail 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Diagnostics; 6 | using JetBrains.Annotations; 7 | 8 | /// 9 | /// Represents an unmanaged _MonoClassField instance in a Mono process. This object describes a field in a 10 | /// managed class or struct. The .NET equivalent is . 11 | /// See: _MonoImage in https://github.com/Unity-Technologies/mono/blob/unity-master/mono/metadata/class-internals.h. 12 | /// 13 | [PublicAPI] 14 | [DebuggerDisplay( 15 | "Field: {" + nameof(FieldDefinition.Offset) + "} - {" + nameof(FieldDefinition.Name) + "}")] 16 | public class FieldDefinition : MemoryObject, IFieldDefinition 17 | { 18 | private readonly List genericTypeArguments; 19 | 20 | public FieldDefinition([NotNull] TypeDefinition declaringType, IntPtr address) 21 | : base((declaringType ?? throw new ArgumentNullException(nameof(declaringType))).Image, address) 22 | { 23 | this.DeclaringType = declaringType; 24 | 25 | // MonoType *type; 26 | this.TypeInfo = new TypeInfo(declaringType.Image, this.ReadPtr(0x0)); 27 | 28 | // MonoType *name; 29 | this.Name = this.ReadString(this.Process.SizeOfPtr); 30 | 31 | // wee need to skip MonoClass *parent field so we add 32 | // 3 pointer sizes (*type, *name, *parent) to the base address 33 | this.Offset = this.ReadInt32(this.Process.SizeOfPtr * 3); 34 | 35 | // Get the generic type arguments 36 | if (this.TypeInfo.TypeCode == TypeCode.GENERICINST) 37 | { 38 | var monoGenericClassAddress = this.TypeInfo.Data; 39 | var monoClassAddress = this.Process.ReadPtr(monoGenericClassAddress); 40 | this.Image.GetTypeDefinition(monoClassAddress); 41 | 42 | var monoGenericContainerPtr = monoClassAddress + this.Process.MonoLibraryOffsets.TypeDefinitionGenericContainer; 43 | var monoGenericContainerAddress = this.Process.ReadPtr(monoGenericContainerPtr); 44 | 45 | var monoGenericContextPtr = monoGenericClassAddress + this.Process.SizeOfPtr; 46 | var monoGenericInsPtr = this.Process.ReadPtr(monoGenericContextPtr); 47 | 48 | // var argumentCount = this.Process.ReadInt32(monoGenericInsPtr + 0x4); 49 | var argumentCount = this.Process.ReadInt32(monoGenericContainerAddress + (4 * this.Process.SizeOfPtr)); 50 | var typeArgVPtr = monoGenericInsPtr + 0x8; 51 | this.genericTypeArguments = new List(argumentCount); 52 | for (int i = 0; i < argumentCount; i++) 53 | { 54 | var genericTypeArgumentPtr = this.Process.ReadPtr(typeArgVPtr + (i * this.Process.SizeOfPtr)); 55 | this.genericTypeArguments.Add(new TypeInfo(this.Image, genericTypeArgumentPtr)); 56 | } 57 | } 58 | else 59 | { 60 | this.genericTypeArguments = null; 61 | } 62 | } 63 | 64 | ITypeDefinition IFieldDefinition.DeclaringType => this.DeclaringType; 65 | 66 | public string Name { get; } 67 | 68 | ITypeInfo IFieldDefinition.TypeInfo => this.TypeInfo; 69 | 70 | public TypeDefinition DeclaringType { get; } 71 | 72 | public int Offset { get; set; } 73 | 74 | public TypeInfo TypeInfo { get; } 75 | 76 | public TValue GetValue(IntPtr address) 77 | { 78 | return this.GetValue(this.genericTypeArguments, address); 79 | } 80 | 81 | public TValue GetValue(List genericTypeArguments, IntPtr address) 82 | { 83 | int offset = this.DeclaringType.IsValueType && !this.TypeInfo.IsStatic ? this.Offset - (this.Process.SizeOfPtr * 2) : this.Offset; 84 | return (TValue)this.TypeInfo.GetValue(this.genericTypeArguments ?? genericTypeArguments, address + offset); 85 | } 86 | } 87 | } -------------------------------------------------------------------------------- /src/HackF5.UnitySpy/Util/MemoryReadingUtils.cs: -------------------------------------------------------------------------------- 1 | namespace HackF5.UnitySpy.Util 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using System.IO; 6 | using System.Text; 7 | using HackF5.UnitySpy.ProcessFacade; 8 | 9 | public class MemoryReadingUtils 10 | { 11 | private ProcessFacade process; 12 | 13 | private List pointersShown = new List(); 14 | 15 | public MemoryReadingUtils(ProcessFacade process) 16 | { 17 | this.process = process; 18 | } 19 | 20 | public void ReadMemory(IntPtr address, int length, int stepSize = 4, int recursiveDepth = 0) 21 | { 22 | StringBuilder strBuilder = new StringBuilder(); 23 | 24 | this.ReadMemoryRecursive(address, length, stepSize, recursiveDepth, strBuilder); 25 | 26 | File.WriteAllText("Memory Dump - " + DateTime.Now.ToString("yyyy-dd-M--HH-mm-ss.fff") + ".txt", strBuilder.ToString()); 27 | } 28 | 29 | private void ReadMemoryRecursive(IntPtr address, int length, int stepSize, int recursiveDepth, StringBuilder strBuilder) 30 | { 31 | for (int i = 0; i < length; i += stepSize) 32 | { 33 | this.SingleReadMemoryRecursive(IntPtr.Add(address, i), length, stepSize, recursiveDepth, strBuilder); 34 | } 35 | } 36 | 37 | private void SingleReadMemoryRecursive(IntPtr address, int length, int stepSize, int recursiveDepth, StringBuilder strBuilder) 38 | { 39 | string addressStr = address.ToString("X"); 40 | 41 | strBuilder.AppendLine("========================================== Reading Memory at " + addressStr + " Depth = " + recursiveDepth + " ========================================== "); 42 | 43 | var ptr = IntPtr.Zero; 44 | if (address != IntPtr.Zero) 45 | { 46 | try 47 | { 48 | ptr = this.process.ReadPtr(address); 49 | } 50 | catch 51 | { 52 | } 53 | } 54 | 55 | try 56 | { 57 | strBuilder.AppendLine("Value as int32: " + this.process.ReadInt32(address)); 58 | strBuilder.AppendLine("Value as uint32: " + this.process.ReadUInt32(address)); 59 | strBuilder.AppendLine("Value as pointer32: " + this.process.ReadUInt32(address).ToString("X")); 60 | strBuilder.AppendLine("Value as pointer64: " + this.process.ReadUInt64(address).ToString("X")); 61 | 62 | byte[] stringBytes = new byte[stepSize]; 63 | for (int i = 0; i < stepSize; i++) 64 | { 65 | stringBytes[i] = this.process.ReadByte(address + i); 66 | } 67 | 68 | strBuilder.AppendLine("Value as string: " + stringBytes.ToAsciiString()); 69 | strBuilder.AppendLine("Value as string (Unicode): " + Encoding.Unicode.GetString(stringBytes, 0, stepSize)); 70 | } 71 | catch (Exception) 72 | { 73 | strBuilder.AppendLine("No possible values found"); 74 | return; 75 | } 76 | 77 | if (ptr != IntPtr.Zero) 78 | { 79 | if (this.pointersShown.Contains(ptr)) 80 | { 81 | strBuilder.AppendLine("Pointer already shown: " + ptr); 82 | } 83 | else 84 | { 85 | try 86 | { 87 | strBuilder.AppendLine("Value as char *: " + this.process.ReadAsciiString(ptr)); 88 | } 89 | catch 90 | { 91 | } 92 | 93 | if (recursiveDepth > 0) 94 | { 95 | this.ReadMemoryRecursive(ptr, length, stepSize, recursiveDepth - 1, strBuilder); 96 | } 97 | 98 | this.pointersShown.Add(ptr); 99 | } 100 | } 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Build and upload 2 | 3 | on: 4 | push: 5 | branches: 6 | - "master" 7 | 8 | jobs: 9 | release: 10 | name: Create Release 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | - name: Get Version 15 | id: projectversion 16 | run: echo "::set-output name=version::$(node getVersion.js)" 17 | - name: Create Release 18 | id: create_release 19 | uses: actions/create-release@v1 20 | env: 21 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 22 | with: 23 | tag_name: ${{ steps.projectversion.outputs.version }} 24 | release_name: v${{ steps.projectversion.outputs.version }} 25 | draft: false 26 | prerelease: false 27 | build-windows: 28 | runs-on: [ windows-latest ] 29 | needs: [ release ] 30 | strategy: 31 | matrix: 32 | dotnet-version: [ '8.0.x' ] 33 | 34 | steps: 35 | - name: "Setup node" 36 | uses: actions/setup-node@v1 37 | with: 38 | node-version: '12' 39 | - uses: actions/checkout@v2 40 | - name: Setup .NET Core SDK ${{ matrix.dotnet-version }} 41 | uses: actions/setup-dotnet@v1.7.2 42 | with: 43 | dotnet-version: ${{ matrix.dotnet-version }} 44 | 45 | - name: Build 46 | run: dotnet publish ./src/mtga-tracker-daemon -r win-x64 --self-contained 47 | 48 | - name: Zip the Build 49 | run: | 50 | mv "./src/mtga-tracker-daemon/bin/Release/net8.0/win-x64/publish" "./src/mtga-tracker-daemon/bin/Release/net8.0/win-x64/bin" 51 | 7z a -tzip "mtga-tracker-daemon-${{ runner.os }}.zip" "./src/mtga-tracker-daemon/bin/Release/net8.0/win-x64/bin" 52 | 53 | - name: Gets latest created release info 54 | id: latest_release_info 55 | uses: jossef/action-latest-release-info@v1.1.0 56 | env: 57 | GITHUB_TOKEN: ${{ github.token }} 58 | 59 | - name: Upload Release Asset 60 | uses: actions/upload-release-asset@v1 61 | env: 62 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 63 | with: 64 | upload_url: ${{ steps.latest_release_info.outputs.upload_url }} 65 | asset_path: ./mtga-tracker-daemon-${{ runner.os }}.zip 66 | asset_name: mtga-tracker-daemon-${{ runner.os }}.zip 67 | asset_content_type: application/zip 68 | build-linux: 69 | runs-on: [ubuntu-latest] 70 | needs: [ release ] 71 | strategy: 72 | matrix: 73 | dotnet-version: [ '8.0.x' ] 74 | 75 | steps: 76 | - name: "Setup node" 77 | uses: actions/setup-node@v1 78 | with: 79 | node-version: '12' 80 | - uses: actions/checkout@v2 81 | - name: Setup .NET Core SDK ${{ matrix.dotnet-version }} 82 | uses: actions/setup-dotnet@v1.7.2 83 | with: 84 | dotnet-version: ${{ matrix.dotnet-version }} 85 | 86 | - name: Build 87 | run: dotnet publish ./src/mtga-tracker-daemon -r linux-x64 --self-contained 88 | 89 | - name: Add deploy artifacts 90 | run: | 91 | mkdir -p deploy/linux/bin 92 | mv -v src/mtga-tracker-daemon/bin/Release/net8.0/linux-x64/publish/* deploy/linux/bin 93 | 94 | - name: Zip the Build 95 | run: cd deploy/linux/ && tar czvf "../../mtga-tracker-daemon-${{ runner.os }}.tar.gz" -C ../linux * --transform s/publish/bin/ && cd ../.. 96 | 97 | - name: Gets latest created release info 98 | id: latest_release_info 99 | uses: jossef/action-latest-release-info@v1.1.0 100 | env: 101 | GITHUB_TOKEN: ${{ github.token }} 102 | 103 | - name: Upload Release Asset 104 | uses: actions/upload-release-asset@v1 105 | env: 106 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 107 | with: 108 | upload_url: ${{ steps.latest_release_info.outputs.upload_url }} 109 | asset_path: ./mtga-tracker-daemon-${{ runner.os }}.tar.gz 110 | asset_name: mtga-tracker-daemon-${{ runner.os }}.tar.gz 111 | asset_content_type: application/zip 112 | -------------------------------------------------------------------------------- /src/HackF5.UnitySpy/Detail/AssemblyImage.cs: -------------------------------------------------------------------------------- 1 | namespace HackF5.UnitySpy.Detail 2 | { 3 | using System; 4 | using System.Collections.Concurrent; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using HackF5.UnitySpy.ProcessFacade; 8 | using JetBrains.Annotations; 9 | 10 | /// 11 | /// Represents an unmanaged _MonoImage instance in a Mono process. This object describes a managed assembly. 12 | /// The .NET equivalent is . 13 | /// See: _MonoImage in https://github.com/Unity-Technologies/mono/blob/unity-master/mono/metadata/metadata-internals.h. 14 | /// 15 | [PublicAPI] 16 | public class AssemblyImage : MemoryObject, IAssemblyImage 17 | { 18 | private readonly Dictionary typeDefinitionsByFullName = 19 | new Dictionary(); 20 | 21 | private readonly ConcurrentDictionary typeDefinitionsByAddress; 22 | 23 | public AssemblyImage(UnityProcessFacade process, IntPtr address) 24 | : base(null, address) 25 | { 26 | this.Process = process; 27 | 28 | this.typeDefinitionsByAddress = this.CreateTypeDefinitions(); 29 | 30 | foreach (var definition in this.TypeDefinitions) 31 | { 32 | definition.Init(); 33 | } 34 | 35 | foreach (var definition in this.TypeDefinitions) 36 | { 37 | if (definition.FullName.Contains("`")) 38 | { 39 | // ignore generic classes as they have name clashes. in order to make them unique these it would be 40 | // necessary to examine the information held in TypeInfo.Data. see 41 | // ProcessFacade.ReadManagedGenericObject for moral support. 42 | continue; 43 | } 44 | 45 | if (!this.typeDefinitionsByFullName.ContainsKey(definition.FullName)) 46 | { 47 | this.typeDefinitionsByFullName.Add(definition.FullName, definition); 48 | } 49 | } 50 | } 51 | 52 | IEnumerable IAssemblyImage.TypeDefinitions => this.TypeDefinitions; 53 | 54 | public IEnumerable TypeDefinitions => 55 | this.typeDefinitionsByAddress.ToArray().Select(k => k.Value); 56 | 57 | public override AssemblyImage Image => this; 58 | 59 | public override UnityProcessFacade Process { get; } 60 | 61 | public dynamic this[string fullTypeName] => this.GetTypeDefinition(fullTypeName); 62 | 63 | ITypeDefinition IAssemblyImage.GetTypeDefinition(string fullTypeName) => this.GetTypeDefinition(fullTypeName); 64 | 65 | public TypeDefinition GetTypeDefinition(string fullTypeName) => 66 | this.typeDefinitionsByFullName.TryGetValue(fullTypeName, out var d) ? d : default; 67 | 68 | public TypeDefinition GetTypeDefinition(IntPtr address) 69 | { 70 | if (address == IntPtr.Zero) 71 | { 72 | return default; 73 | } 74 | 75 | return this.typeDefinitionsByAddress.GetOrAdd( 76 | address, 77 | key => new TypeDefinition(this, key)); 78 | } 79 | 80 | private ConcurrentDictionary CreateTypeDefinitions() 81 | { 82 | var definitions = new ConcurrentDictionary(); 83 | int classCache = this.Process.MonoLibraryOffsets.ImageClassCache; 84 | var classCacheSize = this.ReadUInt32(classCache + this.Process.MonoLibraryOffsets.HashTableSize); 85 | var classCacheTableArray = this.ReadPtr(classCache + this.Process.MonoLibraryOffsets.HashTableTable); 86 | 87 | for (var tableItem = 0; 88 | tableItem < (classCacheSize * this.Process.SizeOfPtr); 89 | tableItem += this.Process.SizeOfPtr) 90 | { 91 | for (var definition = this.Process.ReadPtr(classCacheTableArray + tableItem); 92 | definition != IntPtr.Zero; 93 | definition = this.Process.ReadPtr(definition + this.Process.MonoLibraryOffsets.TypeDefinitionNextClassCache)) 94 | { 95 | definitions.GetOrAdd(definition, new TypeDefinition(this, definition)); 96 | } 97 | } 98 | 99 | return definitions; 100 | } 101 | } 102 | } -------------------------------------------------------------------------------- /src/HackF5.UnitySpy/ProcessFacade/ProcessFacadeLinux.cs: -------------------------------------------------------------------------------- 1 | namespace HackF5.UnitySpy.ProcessFacade 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using System.IO; 6 | using JetBrains.Annotations; 7 | 8 | /// 9 | /// A Linux specific facade over a process that provides access to its memory space. 10 | /// 11 | [PublicAPI] 12 | public abstract class ProcessFacadeLinux : ProcessFacade 13 | { 14 | private readonly List mappings; 15 | 16 | public ProcessFacadeLinux(int processId) 17 | : this($"/proc/{processId}/maps") 18 | { 19 | } 20 | 21 | public ProcessFacadeLinux(string mapsFilePath) 22 | { 23 | string[] mappingsInFile = File.ReadAllLines(mapsFilePath); 24 | this.mappings = new List(mappingsInFile.Length); 25 | string[] lineColumnValues; 26 | string[] memoryRegion; 27 | string name; 28 | 29 | foreach (var line in mappingsInFile) 30 | { 31 | lineColumnValues = line.Split(' '); 32 | memoryRegion = lineColumnValues[0].Split('-'); 33 | if (lineColumnValues.Length > 6) 34 | { 35 | name = line.Substring(73); 36 | } 37 | else 38 | { 39 | name = string.Empty; 40 | } 41 | 42 | this.mappings.Add(new MemoryMapping(memoryRegion[0], memoryRegion[1], name, lineColumnValues[4] != "0")); 43 | } 44 | } 45 | 46 | public override void ReadProcessMemory( 47 | byte[] buffer, 48 | IntPtr processAddress, 49 | bool allowPartialRead = false, 50 | int? size = default) 51 | { 52 | if (buffer == null) 53 | { 54 | throw new ArgumentNullException("the buffer parameter cannot be null"); 55 | } 56 | 57 | int length = size ?? buffer.Length; 58 | if (this.mappings.Exists(mapping => mapping.Contains(processAddress))) 59 | { 60 | this.ReadProcessMemory(buffer, processAddress, length); 61 | } 62 | else 63 | { 64 | Console.Error.WriteLine($"Attempting to read unmapped address {processAddress.ToString("X")} + {length}"); 65 | for (int i = 0; i < buffer.Length; i++) 66 | { 67 | buffer[i] = 0; 68 | } 69 | } 70 | } 71 | 72 | public override ModuleInfo GetModule(string moduleName) 73 | { 74 | int mappingIndex = this.mappings.FindIndex(mapping => mapping.ModuleName.EndsWith(moduleName)); 75 | 76 | if (mappingIndex < 0) 77 | { 78 | throw new Exception($"{moduleName} module not found"); 79 | } 80 | 81 | IntPtr startingAddress = this.mappings[mappingIndex].StartAddress; 82 | string fullModuleName = this.mappings[mappingIndex].ModuleName; 83 | 84 | while (mappingIndex < this.mappings.Count && (!this.mappings[mappingIndex].IsStartingModule || this.mappings[mappingIndex].ModuleName == fullModuleName)) 85 | { 86 | mappingIndex++; 87 | } 88 | 89 | mappingIndex--; 90 | uint size = Convert.ToUInt32(MemoryMapping.GetSize(startingAddress, this.mappings[mappingIndex].EndAddress)); 91 | 92 | return new ModuleInfo(moduleName, startingAddress, size, fullModuleName); 93 | } 94 | 95 | public string GetModulePath(string moduleName) 96 | { 97 | int mappingIndex = this.mappings.FindIndex(mapping => mapping.ModuleName.EndsWith(moduleName)); 98 | 99 | if (mappingIndex < 0) 100 | { 101 | throw new Exception($"{moduleName} module not found"); 102 | } 103 | 104 | return this.mappings[mappingIndex].ModuleName; 105 | } 106 | 107 | protected abstract void ReadProcessMemory( 108 | byte[] buffer, 109 | IntPtr processAddress, 110 | int size); 111 | 112 | protected struct MemoryMapping 113 | { 114 | public MemoryMapping(string startAddress, string endAddress, string moduleName, bool isStartingModule) 115 | : this( 116 | new IntPtr(Convert.ToInt64(startAddress, 16)), 117 | new IntPtr(Convert.ToInt64(endAddress, 16)), 118 | moduleName, 119 | isStartingModule) 120 | { 121 | } 122 | 123 | public MemoryMapping(IntPtr startAddress, IntPtr endAddress, string moduleName, bool isStartingModule) 124 | { 125 | this.StartAddress = startAddress; 126 | this.EndAddress = endAddress; 127 | this.ModuleName = moduleName; 128 | this.IsStartingModule = isStartingModule; 129 | } 130 | 131 | public IntPtr StartAddress { get; set; } 132 | 133 | public IntPtr EndAddress { get; set; } 134 | 135 | public string ModuleName { get; set; } 136 | 137 | public bool IsStartingModule { get; set; } 138 | 139 | public long Size => MemoryMapping.GetSize(this.StartAddress, this.EndAddress); 140 | 141 | public static long GetSize(IntPtr startAddress, IntPtr endAddress) 142 | => endAddress.ToInt64() - startAddress.ToInt64(); 143 | 144 | public bool Contains(IntPtr address, int length = 0) 145 | { 146 | long addressAsLong = address.ToInt64(); 147 | return addressAsLong >= this.StartAddress.ToInt64() && addressAsLong + length < this.EndAddress.ToInt64(); 148 | } 149 | } 150 | } 151 | } -------------------------------------------------------------------------------- /src/HackF5.UnitySpy/ProcessFacade/ProcessFacadeWindows.cs: -------------------------------------------------------------------------------- 1 | namespace HackF5.UnitySpy.ProcessFacade 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using System.ComponentModel; 6 | using System.Diagnostics; 7 | using System.IO; 8 | using System.Linq; 9 | using System.Runtime.InteropServices; 10 | using System.Text; 11 | using JetBrains.Annotations; 12 | 13 | /// 14 | /// A Windows specific facade over a process that provides access to its memory space. 15 | /// 16 | [PublicAPI] 17 | public class ProcessFacadeWindows : ProcessFacade 18 | { 19 | private const string PsApiDll = "psapi.dll"; 20 | 21 | private readonly Process process; 22 | 23 | public ProcessFacadeWindows(Process process) 24 | : base() 25 | { 26 | this.process = process; 27 | } 28 | 29 | public Process Process => this.process; 30 | 31 | public override void ReadProcessMemory( 32 | byte[] buffer, 33 | IntPtr processAddress, 34 | bool allowPartialRead = false, 35 | int? size = default) 36 | { 37 | if (buffer == null) 38 | { 39 | throw new ArgumentNullException("the buffer parameter cannot be null"); 40 | } 41 | 42 | var bufferHandle = GCHandle.Alloc(buffer, GCHandleType.Pinned); 43 | 44 | try 45 | { 46 | var bufferPointer = Marshal.UnsafeAddrOfPinnedArrayElement(buffer, 0); 47 | if (!ProcessFacadeWindows.ReadProcessMemory( 48 | this.process.Handle, 49 | processAddress, 50 | bufferPointer, 51 | size ?? buffer.Length, 52 | out _)) 53 | { 54 | var error = Marshal.GetLastWin32Error(); 55 | if ((error == 299) && allowPartialRead) 56 | { 57 | return; 58 | } 59 | 60 | throw new Win32Exception(error); 61 | } 62 | } 63 | finally 64 | { 65 | bufferHandle.Free(); 66 | } 67 | } 68 | 69 | public string GetMainModuleFileName(int buffer = 1024) 70 | { 71 | var fileNameBuilder = new StringBuilder(buffer); 72 | uint bufferLength = (uint)fileNameBuilder.Capacity + 1; 73 | return QueryFullProcessImageName(this.process.Handle, 0, fileNameBuilder, ref bufferLength) ? 74 | fileNameBuilder.ToString() : 75 | null; 76 | } 77 | 78 | // https://stackoverflow.com/questions/36431220/getting-a-list-of-dlls-currently-loaded-in-a-process-c-sharp 79 | // TODO add check for matching platforms and implement the following code while keeping the existing one otherwise: 80 | // This can be done with this if the process is running in 64 bits mode (and UnitySpy too of course) 81 | // foreach(ProcessModule module in process.Process.Modules) 82 | // { 83 | // if(module.ModuleName == moduleName) 84 | // { 85 | // return new ModuleInfo(module.ModuleName, module.BaseAddress, module.ModuleMemorySize); 86 | // } 87 | // } 88 | public override ModuleInfo GetModule(string moduleName) 89 | { 90 | var modulePointers = this.GetModulePointers(); 91 | 92 | // Collect modules from the process 93 | var modules = new List(); 94 | foreach (var modulePointer in modulePointers) 95 | { 96 | var moduleFilePath = new StringBuilder(1024); 97 | var errorCode = GetModuleFileNameEx( 98 | this.process.Handle, 99 | modulePointer, 100 | moduleFilePath, 101 | (uint)moduleFilePath.Capacity); 102 | 103 | if (errorCode == 0) 104 | { 105 | throw new COMException("Failed to get module file name.", Marshal.GetLastWin32Error()); 106 | } 107 | 108 | var currentModuleName = Path.GetFileName(moduleFilePath.ToString()); 109 | GetModuleInformation( 110 | this.process.Handle, 111 | modulePointer, 112 | out var moduleInformation, 113 | (uint)(IntPtr.Size * modulePointers.Length)); 114 | 115 | // Convert to a normalized module and add it to our list 116 | var module = new ModuleInfo(currentModuleName, moduleInformation.BaseOfDll, moduleInformation.SizeInBytes, moduleFilePath.ToString()); 117 | modules.Add(module); 118 | } 119 | 120 | return modules.FirstOrDefault(module => module.ModuleName == moduleName); 121 | } 122 | 123 | [DllImport(ProcessFacadeWindows.PsApiDll, SetLastError = true)] 124 | private static extern uint GetModuleFileNameEx( 125 | IntPtr hProcess, 126 | IntPtr hModule, 127 | [Out] StringBuilder lpBaseName, 128 | [In] [MarshalAs(UnmanagedType.U4)] uint nSize); 129 | 130 | [DllImport(ProcessFacadeWindows.PsApiDll, SetLastError = true)] 131 | private static extern bool GetModuleInformation( 132 | IntPtr hProcess, 133 | IntPtr hModule, 134 | out ModuleInformation lpModInfo, 135 | uint cb); 136 | 137 | // https://stackoverflow.com/questions/36431220/getting-a-list-of-dlls-currently-loaded-in-a-process-c-sharp 138 | [DllImport(ProcessFacadeWindows.PsApiDll, SetLastError = true)] 139 | private static extern bool EnumProcessModulesEx( 140 | IntPtr hProcess, 141 | [MarshalAs(UnmanagedType.LPArray, ArraySubType = UnmanagedType.U4)] [In] [Out] 142 | IntPtr[] lphModule, 143 | int cb, 144 | [MarshalAs(UnmanagedType.U4)] out int lpCbNeeded, 145 | uint dwFilterFlag); 146 | 147 | [DllImport("kernel32", SetLastError = true)] 148 | private static extern bool ReadProcessMemory( 149 | IntPtr hProcess, 150 | IntPtr lpBaseAddress, 151 | IntPtr lpBuffer, 152 | int nSize, 153 | out IntPtr lpNumberOfBytesRead); 154 | 155 | // https://stackoverflow.com/questions/5497064/how-to-get-the-full-path-of-running-process 156 | [DllImport("Kernel32.dll")] 157 | private static extern bool QueryFullProcessImageName([In] IntPtr hProcess, [In] uint dwFlags, [Out] StringBuilder lpExeName, [In, Out] ref uint lpdwSize); 158 | 159 | private IntPtr[] GetModulePointers() 160 | { 161 | var modulePointers = new IntPtr[2048 * this.SizeOfPtr]; 162 | 163 | // Determine number of modules 164 | if (!EnumProcessModulesEx( 165 | this.process.Handle, 166 | modulePointers, 167 | modulePointers.Length, 168 | out var bytesNeeded, 169 | 0x03)) 170 | { 171 | throw new COMException( 172 | "Failed to read modules from the external process.", 173 | Marshal.GetLastWin32Error()); 174 | } 175 | 176 | var result = new IntPtr[bytesNeeded / IntPtr.Size]; 177 | Buffer.BlockCopy(modulePointers, 0, result, 0, bytesNeeded); 178 | return result; 179 | } 180 | 181 | [StructLayout(LayoutKind.Sequential)] 182 | private struct ModuleInformation 183 | { 184 | public readonly IntPtr BaseOfDll; 185 | 186 | public readonly uint SizeInBytes; 187 | 188 | private readonly IntPtr entryPoint; 189 | } 190 | } 191 | } -------------------------------------------------------------------------------- /src/HackF5.UnitySpy/ProcessFacade/ProcessFacadeClient.cs: -------------------------------------------------------------------------------- 1 | namespace HackF5.UnitySpy.ProcessFacade 2 | { 3 | using System; 4 | using System.Net; 5 | using System.Net.Sockets; 6 | using System.Runtime.InteropServices; 7 | using System.Text; 8 | using HackF5.UnitySpy.Util; 9 | using JetBrains.Annotations; 10 | 11 | /// 12 | /// A Windows specific facade over a process that provides access to its memory space. 13 | /// 14 | [PublicAPI] 15 | public class ProcessFacadeClient 16 | { 17 | public const int Port = 39185; 18 | public const int BufferSize = 4096; 19 | public const int RequestSize = 17; 20 | public const byte ReadMemoryRequestType = 0; 21 | public const byte GetModuleRequestType = 1; 22 | 23 | private int processId; 24 | 25 | private byte[] internalBuffer = new byte[BufferSize]; 26 | 27 | private Socket socket; 28 | 29 | public ProcessFacadeClient(int processId) 30 | { 31 | this.processId = processId; 32 | } 33 | 34 | public void ReadProcessMemory( 35 | byte[] buffer, 36 | IntPtr processAddress, 37 | int length) 38 | { 39 | if (buffer == null) 40 | { 41 | throw new ArgumentNullException("the buffer parameter cannot be null"); 42 | } 43 | 44 | lock (this) 45 | { 46 | int bufferIndex = 0; 47 | Request request = new Request(ReadMemoryRequestType, this.processId, processAddress, length); 48 | 49 | // For debbuging 50 | // Console.WriteLine($"Requested address = {processAddress.ToString("X")}, length = {length}"); 51 | 52 | int bytesRec = 0; 53 | try 54 | { 55 | if (this.socket == null) 56 | { 57 | this.Connect(); 58 | } 59 | 60 | // Send the data through the socket. 61 | int bytesSent = this.socket.Send(request.GetBytes()); 62 | 63 | // Receive the response from the remote device. 64 | do 65 | { 66 | bytesRec = this.socket.Receive(this.internalBuffer); 67 | Array.Copy(this.internalBuffer, 0, buffer, bufferIndex, bytesRec); 68 | bufferIndex += bytesRec; 69 | length -= bytesRec; 70 | } 71 | while (length > 0); 72 | } 73 | catch (ArgumentNullException ane) 74 | { 75 | Console.WriteLine("ArgumentNullException : {0}", ane.ToString()); 76 | this.CloseConnection(); 77 | } 78 | catch (ArgumentException ae) 79 | { 80 | Console.WriteLine($"address = {processAddress.ToString("X")}, length = {length}, Bytes Rec = {bytesRec}, bufferIndex = {bufferIndex}, buffer length = {buffer.Length}"); 81 | Console.WriteLine("ArgumentException : {0}", ae.ToString()); 82 | this.CloseConnection(); 83 | } 84 | catch (SocketException se) 85 | { 86 | Console.WriteLine("SocketException : {0}", se.ToString()); 87 | this.CloseConnection(); 88 | } 89 | catch (Exception e) 90 | { 91 | Console.WriteLine("Unexpected exception : {0}", e.ToString()); 92 | this.CloseConnection(); 93 | } 94 | } 95 | } 96 | 97 | public ModuleInfo GetModuleInfo(string moduleName) 98 | { 99 | lock (this) 100 | { 101 | byte[] moduleNameInAscii = Encoding.ASCII.GetBytes(moduleName); 102 | Request request = new Request(GetModuleRequestType, this.processId, IntPtr.Zero, moduleNameInAscii.Length); 103 | try 104 | { 105 | if (this.socket == null) 106 | { 107 | this.Connect(); 108 | } 109 | 110 | // Send the data through the socket. 111 | int bytesSent = this.socket.Send(request.GetBytes()); 112 | bytesSent = this.socket.Send(moduleNameInAscii); 113 | 114 | // Receive the base address 115 | this.socket.Receive(this.internalBuffer, 8, SocketFlags.None); 116 | IntPtr baseAddress = (IntPtr)this.internalBuffer.ToUInt64(); 117 | 118 | // Receive the size 119 | this.socket.Receive(this.internalBuffer, 8, SocketFlags.None); 120 | uint size = this.internalBuffer.ToUInt32(); 121 | 122 | string path = this.GetString(); 123 | return new ModuleInfo(moduleName, baseAddress, size, path); 124 | } 125 | catch (ArgumentNullException ane) 126 | { 127 | Console.WriteLine("ArgumentNullException : {0}", ane.ToString()); 128 | this.CloseConnection(); 129 | } 130 | catch (SocketException se) 131 | { 132 | Console.WriteLine("SocketException : {0}", se.ToString()); 133 | this.CloseConnection(); 134 | } 135 | catch (Exception e) 136 | { 137 | Console.WriteLine("Unexpected exception : {0}", e.ToString()); 138 | this.CloseConnection(); 139 | } 140 | 141 | throw new Exception($"Could not find the {moduleName} module"); 142 | } 143 | } 144 | 145 | private string GetString() 146 | { 147 | // Receive the length of the string 148 | int bytesRec = this.socket.Receive(this.internalBuffer, 4, SocketFlags.None); 149 | 150 | int length = this.internalBuffer.ToInt32(); 151 | 152 | byte[] buffer = new byte[length]; 153 | int bufferIndex = 0; 154 | do 155 | { 156 | bytesRec = this.socket.Receive(this.internalBuffer); 157 | 158 | Array.Copy(this.internalBuffer, 0, buffer, bufferIndex, bytesRec); 159 | bufferIndex += bytesRec; 160 | length -= bytesRec; 161 | } 162 | while (length > 0); 163 | return buffer.ToAsciiString(); 164 | } 165 | 166 | // Connect to the server 167 | private void Connect() 168 | { 169 | Console.WriteLine("Connecting to server..."); 170 | 171 | // Establish the remote endpoint for the socket. 172 | IPHostEntry localhost = Dns.GetHostEntry(Dns.GetHostName()); 173 | IPAddress ipAddress = localhost.AddressList[0]; 174 | IPEndPoint serverEndPoint = new IPEndPoint(ipAddress, Port); 175 | 176 | // Create a TCP/IP socket. 177 | this.socket = new Socket(ipAddress.AddressFamily, SocketType.Stream, ProtocolType.Tcp); 178 | this.socket.Connect(serverEndPoint); 179 | } 180 | 181 | private void CloseConnection() 182 | { 183 | if (this.socket != null) 184 | { 185 | Console.WriteLine("Disconnecting from server..."); 186 | try 187 | { 188 | // Release the socket 189 | this.socket.Shutdown(SocketShutdown.Both); 190 | } 191 | catch (SocketException se) 192 | { 193 | Console.WriteLine("SocketException : {0}", se.ToString()); 194 | } 195 | catch (Exception e) 196 | { 197 | Console.WriteLine("Unexpected exception : {0}", e.ToString()); 198 | } 199 | finally 200 | { 201 | this.socket.Close(); 202 | this.socket = null; 203 | } 204 | } 205 | } 206 | 207 | [StructLayout(LayoutKind.Sequential)] 208 | private struct Request 209 | { 210 | private byte type; 211 | private int pid; 212 | private IntPtr address; 213 | private int size; 214 | 215 | public Request(byte type, int pid, IntPtr address, int size) 216 | { 217 | this.type = type; 218 | this.pid = pid; 219 | this.address = address; 220 | this.size = size; 221 | } 222 | 223 | public byte[] GetBytes() 224 | { 225 | byte[] arr = new byte[RequestSize]; 226 | arr[0] = this.type; 227 | 228 | IntPtr ptr = Marshal.AllocHGlobal(RequestSize); 229 | Marshal.StructureToPtr(this, ptr, true); 230 | 231 | // start at 4 because the byte type in C# is just an int 232 | Marshal.Copy(ptr + 4, arr, 1, RequestSize - 1); 233 | Marshal.FreeHGlobal(ptr); 234 | return arr; 235 | } 236 | 237 | public override string ToString() 238 | { 239 | return $"Address = {this.address.ToString("X")}, size = {this.size}"; 240 | } 241 | } 242 | } 243 | } -------------------------------------------------------------------------------- /src/HackF5.UnitySpy/AssemblyImageFactory.cs: -------------------------------------------------------------------------------- 1 | namespace HackF5.UnitySpy 2 | { 3 | using System; 4 | using System.IO; 5 | using System.Runtime.InteropServices; 6 | using HackF5.UnitySpy.Detail; 7 | using HackF5.UnitySpy.Offsets; 8 | using HackF5.UnitySpy.ProcessFacade; 9 | using HackF5.UnitySpy.Util; 10 | using JetBrains.Annotations; 11 | 12 | /// 13 | /// A factory that creates instances that provides access into a Unity application's 14 | /// managed memory. 15 | /// SEE: https://github.com/Unity-Technologies/mono. 16 | /// 17 | [PublicAPI] 18 | public static class AssemblyImageFactory 19 | { 20 | public static IAssemblyImage Create([NotNull] UnityProcessFacade process, string assemblyName = "Assembly-CSharp") 21 | { 22 | if (process == null) 23 | { 24 | throw new ArgumentNullException("process parameter cannot be null"); 25 | } 26 | 27 | var monoModule = process.GetMonoModule(); 28 | IntPtr rootDomainFunctionAddress; 29 | if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) 30 | { 31 | rootDomainFunctionAddress = GetRootDomainFunctionAddressMachOFormat(monoModule); 32 | } 33 | else 34 | { 35 | var moduleDump = process.ReadModule(monoModule); 36 | rootDomainFunctionAddress = AssemblyImageFactory.GetRootDomainFunctionAddressPEFormat(moduleDump, monoModule, process.Is64Bits); 37 | } 38 | 39 | return AssemblyImageFactory.GetAssemblyImage(process, assemblyName, rootDomainFunctionAddress); 40 | } 41 | 42 | private static AssemblyImage GetAssemblyImage(UnityProcessFacade process, string name, IntPtr rootDomainFunctionAddress) 43 | { 44 | IntPtr domain; 45 | if (process.Is64Bits) 46 | { 47 | int ripPlusOffsetOffset; 48 | int ripValueOffset; 49 | if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) 50 | { 51 | // Offsets taken by decompiling the 64 bits version of libmonobdwgc-2.0.dylib 52 | // 53 | // push rbp 54 | // mov rbp,rsp 55 | // mov rax, [rip + 0x4250ba] 56 | // pop rbp 57 | // ret 58 | // 59 | // These five lines in Hex translate to 60 | // 55 61 | // 4889E5 62 | // 488B05 BA5042 00 63 | // 5D 64 | // C3 65 | // 66 | // So wee need to offset the first seven bytes to get to the relative offset we need to add to rip 67 | // rootDomainFunctionAddress + 7 68 | // 69 | // rip has the current value of the rootDoaminAddress plus the 4 bytes of the first two instructions 70 | // plus the 7 bytes of the rip + offset instruction (mov rax, [rip + 0x4250ba]). 71 | // then we need to add this offsets to get the domain starting address 72 | ripPlusOffsetOffset = 7; 73 | ripValueOffset = 11; 74 | } 75 | else 76 | { 77 | // Offsets taken by decompiling the 64 bits version of mono-2.0-bdwgc.dll 78 | // 79 | // mov rax, [rip + 0x46ad39] 80 | // ret 81 | // 82 | // These two lines in Hex translate to 83 | // 488B05 39AD46 00 84 | // C3 85 | // 86 | // So wee need to offset the first three bytes to get to the relative offset we need to add to rip 87 | // rootDomainFunctionAddress + 3 88 | // 89 | // rip has the current value of the rootDoaminAddress plus the 7 bytes of the first instruction (mov rax, [rip + 0x46ad39]) 90 | // then we need to add this offsets to get the domain starting address 91 | ripPlusOffsetOffset = 3; 92 | ripValueOffset = 7; 93 | } 94 | 95 | var offset = process.ReadInt32(rootDomainFunctionAddress + ripPlusOffsetOffset) + ripValueOffset; 96 | //// pointer to struct of type _MonoDomain 97 | domain = process.ReadPtr(rootDomainFunctionAddress + offset); 98 | } 99 | else 100 | { 101 | var domainAddress = process.ReadPtr(rootDomainFunctionAddress + 1); 102 | //// pointer to struct of type _MonoDomain 103 | domain = process.ReadPtr(domainAddress); 104 | } 105 | //// pointer to array of structs of type _MonoAssembly 106 | var assemblyArrayAddress = process.ReadPtr(domain + process.MonoLibraryOffsets.ReferencedAssemblies); 107 | for (var assemblyAddress = assemblyArrayAddress; 108 | assemblyAddress != IntPtr.Zero; 109 | assemblyAddress = process.ReadPtr(assemblyAddress + process.SizeOfPtr)) 110 | { 111 | var assembly = process.ReadPtr(assemblyAddress); 112 | var assemblyNameAddress = process.ReadPtr(assembly + (process.SizeOfPtr * 2)); 113 | var assemblyName = process.ReadAsciiString(assemblyNameAddress); 114 | if (assemblyName == name) 115 | { 116 | return new AssemblyImage(process, process.ReadPtr(assembly + process.MonoLibraryOffsets.AssemblyImage)); 117 | } 118 | } 119 | 120 | throw new InvalidOperationException($"Unable to find assembly '{name}'"); 121 | } 122 | 123 | private static IntPtr GetRootDomainFunctionAddressPEFormat(byte[] moduleDump, ModuleInfo monoModuleInfo, bool is64Bits) 124 | { 125 | // offsets taken from https://docs.microsoft.com/en-us/windows/desktop/Debug/pe-format 126 | // ReSharper disable once CommentTypo 127 | var startIndex = moduleDump.ToInt32(PEFormatOffsets.Signature); // lfanew 128 | 129 | var exportDirectoryIndex = startIndex + PEFormatOffsets.GetExportDirectoryIndex(is64Bits); 130 | var exportDirectory = moduleDump.ToInt32(exportDirectoryIndex); 131 | 132 | var numberOfFunctions = moduleDump.ToInt32(exportDirectory + PEFormatOffsets.NumberOfFunctions); 133 | var functionAddressArrayIndex = moduleDump.ToInt32(exportDirectory + PEFormatOffsets.FunctionAddressArrayIndex); 134 | var functionNameArrayIndex = moduleDump.ToInt32(exportDirectory + PEFormatOffsets.FunctionNameArrayIndex); 135 | var rootDomainFunctionAddress = IntPtr.Zero; 136 | for (var functionIndex = 0; 137 | functionIndex < numberOfFunctions * PEFormatOffsets.FunctionEntrySize; 138 | functionIndex += PEFormatOffsets.FunctionEntrySize) 139 | { 140 | var functionNameIndex = moduleDump.ToInt32(functionNameArrayIndex + functionIndex); 141 | var functionName = moduleDump.ToAsciiString(functionNameIndex); 142 | if (functionName == "mono_get_root_domain") 143 | { 144 | rootDomainFunctionAddress = monoModuleInfo.BaseAddress 145 | + moduleDump.ToInt32(functionAddressArrayIndex + functionIndex); 146 | 147 | break; 148 | } 149 | } 150 | 151 | if (rootDomainFunctionAddress == IntPtr.Zero) 152 | { 153 | throw new InvalidOperationException("Failed to find mono_get_root_domain function."); 154 | } 155 | 156 | return rootDomainFunctionAddress; 157 | } 158 | 159 | private static IntPtr GetRootDomainFunctionAddressMachOFormat(ModuleInfo monoModuleInfo) 160 | { 161 | var rootDomainFunctionAddress = IntPtr.Zero; 162 | 163 | byte[] moduleFromPath = File.ReadAllBytes(monoModuleInfo.Path); 164 | 165 | int numberOfCommands = moduleFromPath.ToInt32(MachOFormatOffsets.NumberOfCommands); 166 | int offsetToNextCommand = MachOFormatOffsets.LoadCommands; 167 | for (int i = 0; i < numberOfCommands; i++) 168 | { 169 | // Check if load command is LC_SYMTAB 170 | if (moduleFromPath.ToInt32(offsetToNextCommand) == 2) 171 | { 172 | int symbolTableOffset = moduleFromPath.ToInt32(offsetToNextCommand + MachOFormatOffsets.SymbolTableOffset); 173 | int numberOfSymbols = moduleFromPath.ToInt32(offsetToNextCommand + MachOFormatOffsets.NumberOfSymbols); 174 | int stringTableOffset = moduleFromPath.ToInt32(offsetToNextCommand + MachOFormatOffsets.StringTableOffset); 175 | 176 | for (int j = 0; j < numberOfSymbols; j++) 177 | { 178 | int symbolNameOffset = moduleFromPath.ToInt32(symbolTableOffset + (j * MachOFormatOffsets.SizeOfNListItem)); 179 | var symbolName = moduleFromPath.ToAsciiString(stringTableOffset + symbolNameOffset); 180 | 181 | if (symbolName == "_mono_get_root_domain") 182 | { 183 | rootDomainFunctionAddress = monoModuleInfo.BaseAddress 184 | + moduleFromPath.ToInt32(symbolTableOffset + (j * MachOFormatOffsets.SizeOfNListItem) 185 | + MachOFormatOffsets.NListValue); 186 | break; 187 | } 188 | } 189 | 190 | break; 191 | } 192 | else 193 | { 194 | offsetToNextCommand += moduleFromPath.ToInt32(offsetToNextCommand + MachOFormatOffsets.CommandSize); 195 | } 196 | } 197 | 198 | if (rootDomainFunctionAddress == IntPtr.Zero) 199 | { 200 | throw new InvalidOperationException("Failed to find mono_get_root_domain function."); 201 | } 202 | 203 | return rootDomainFunctionAddress; 204 | } 205 | } 206 | } -------------------------------------------------------------------------------- /src/HackF5.UnitySpy/Detail/TypeDefinition.cs: -------------------------------------------------------------------------------- 1 | namespace HackF5.UnitySpy.Detail 2 | { 3 | using System; 4 | using System.Collections.Concurrent; 5 | using System.Collections.Generic; 6 | using System.Collections.ObjectModel; 7 | using System.Diagnostics; 8 | using System.Linq; 9 | using System.Text; 10 | using JetBrains.Annotations; 11 | 12 | /// 13 | /// Represents an unmanaged _MonoClass instance in a Mono process. This object describes the type of a class or 14 | /// struct. The .NET equivalent is . 15 | /// See: _MonoClass in https://github.com/Unity-Technologies/mono/blob/unity-master/mono/metadata/class-internals.h. 16 | /// 17 | [PublicAPI] 18 | [DebuggerDisplay("Class: {" + nameof(TypeDefinition.Name) + "}")] 19 | public class TypeDefinition : MemoryObject, ITypeDefinition 20 | { 21 | private readonly uint bitFields; 22 | 23 | private readonly ConcurrentDictionary<(string @class, string name), FieldDefinition> fieldCache = 24 | new ConcurrentDictionary<(string @class, string name), FieldDefinition>(); 25 | 26 | private readonly int fieldCount; 27 | 28 | private readonly Lazy> lazyFields; 29 | 30 | private readonly Lazy lazyFullName; 31 | 32 | private readonly Lazy lazyNestedIn; 33 | 34 | private readonly Lazy lazyParent; 35 | 36 | private readonly Lazy lazyGeneric; 37 | 38 | private readonly List genericTypeArguments; 39 | 40 | public TypeDefinition([NotNull] AssemblyImage image, IntPtr address) 41 | : base(image, address) 42 | { 43 | if (image == null) 44 | { 45 | throw new ArgumentNullException(nameof(image)); 46 | } 47 | 48 | this.bitFields = this.ReadUInt32(image.Process.MonoLibraryOffsets.TypeDefinitionBitFields); 49 | this.fieldCount = this.ReadInt32(image.Process.MonoLibraryOffsets.TypeDefinitionFieldCount); 50 | this.lazyParent = new Lazy(() => this.GetClassDefinition(image.Process.MonoLibraryOffsets.TypeDefinitionParent)); 51 | this.lazyNestedIn = new Lazy(() => this.GetClassDefinition(image.Process.MonoLibraryOffsets.TypeDefinitionNestedIn)); 52 | this.lazyFullName = new Lazy(this.GetFullName); 53 | this.lazyFields = new Lazy>(this.GetFields); 54 | this.lazyGeneric = new Lazy(this.GetGeneric); 55 | 56 | this.Name = this.ReadString(image.Process.MonoLibraryOffsets.TypeDefinitionName); 57 | this.NamespaceName = this.ReadString(image.Process.MonoLibraryOffsets.TypeDefinitionNamespace); 58 | this.Size = this.ReadInt32(image.Process.MonoLibraryOffsets.TypeDefinitionSize); 59 | var vtablePtr = this.ReadPtr(image.Process.MonoLibraryOffsets.TypeDefinitionRuntimeInfo); 60 | this.VTable = vtablePtr == IntPtr.Zero ? IntPtr.Zero : image.Process.ReadPtr(vtablePtr + image.Process.MonoLibraryOffsets.TypeDefinitionRuntimeInfoDomainVTables); 61 | this.TypeInfo = new TypeInfo(image, this.Address + image.Process.MonoLibraryOffsets.TypeDefinitionByValArg); 62 | this.VTableSize = vtablePtr == IntPtr.Zero ? 0 : this.ReadInt32(image.Process.MonoLibraryOffsets.TypeDefinitionVTableSize); 63 | this.ClassKind = (MonoClassKind)(this.ReadByte(image.Process.MonoLibraryOffsets.TypeDefinitionClassKind) & 0x7); 64 | 65 | // Get the generic type arguments 66 | if (this.TypeInfo.TypeCode == TypeCode.GENERICINST) 67 | { 68 | var monoGenericClassAddress = this.TypeInfo.Data; 69 | var monoClassAddress = this.Process.ReadPtr(monoGenericClassAddress); 70 | this.Image.GetTypeDefinition(monoClassAddress); 71 | 72 | var monoGenericContainerPtr = monoClassAddress + this.Process.MonoLibraryOffsets.TypeDefinitionGenericContainer; 73 | var monoGenericContainerAddress = this.Process.ReadPtr(monoGenericContainerPtr); 74 | 75 | var monoGenericContextPtr = monoGenericClassAddress + this.Process.SizeOfPtr; 76 | var monoGenericInsPtr = this.Process.ReadPtr(monoGenericContextPtr); 77 | 78 | // var argumentCount = this.Process.ReadInt32(monoGenericInsPtr + 0x4); 79 | var argumentCount = this.Process.ReadInt32(monoGenericContainerAddress + (4 * this.Process.SizeOfPtr)); 80 | var typeArgVPtr = monoGenericInsPtr + 0x8; 81 | this.genericTypeArguments = new List(argumentCount); 82 | for (int i = 0; i < argumentCount; i++) 83 | { 84 | var genericTypeArgumentPtr = this.Process.ReadPtr(typeArgVPtr + (i * this.Process.SizeOfPtr)); 85 | this.genericTypeArguments.Add(new TypeInfo(this.Image, monoGenericInsPtr)); 86 | } 87 | } 88 | else 89 | { 90 | this.genericTypeArguments = null; 91 | } 92 | } 93 | 94 | IReadOnlyList ITypeDefinition.Fields => this.Fields; 95 | 96 | public string FullName => this.lazyFullName.Value; 97 | 98 | public bool IsEnum => (this.bitFields & 0x8) == 0x8; 99 | 100 | public bool IsValueType => (this.bitFields & 0x4) == 0x4; 101 | 102 | public string Name { get; } 103 | 104 | public string NamespaceName { get; } 105 | 106 | ITypeInfo ITypeDefinition.TypeInfo => this.TypeInfo; 107 | 108 | public IReadOnlyList Fields => this.lazyFields.Value; 109 | 110 | public TypeDefinition NestedIn => this.lazyNestedIn.Value; 111 | 112 | public TypeDefinition Parent => this.lazyParent.Value; 113 | 114 | public int Size { get; } 115 | 116 | public TypeInfo TypeInfo { get; } 117 | 118 | public IntPtr VTable { get; } 119 | 120 | public int VTableSize { get; } 121 | 122 | public MonoClassKind ClassKind { get; } 123 | 124 | public List GenericTypeArguments 125 | { 126 | get 127 | { 128 | if (this.genericTypeArguments == null && this.NestedIn != null) 129 | { 130 | return this.NestedIn.GenericTypeArguments; 131 | } 132 | else 133 | { 134 | return this.genericTypeArguments; 135 | } 136 | } 137 | } 138 | 139 | public dynamic this[string fieldName] => this.GetStaticValue(fieldName); 140 | 141 | IFieldDefinition ITypeDefinition.GetField(string fieldName, string typeFullName) => 142 | this.GetField(fieldName, typeFullName); 143 | 144 | public TValue GetStaticValue(string fieldName) 145 | { 146 | var field = this.GetField(fieldName, this.FullName) 147 | ?? throw new ArgumentException($"Field '{fieldName}' does not exist in class '{this.FullName}'.", nameof(fieldName)); 148 | 149 | if (!field.TypeInfo.IsStatic) 150 | { 151 | throw new InvalidOperationException($"Field '{fieldName}' is not static in class '{this.FullName}'."); 152 | } 153 | 154 | if (field.TypeInfo.IsConstant) 155 | { 156 | throw new InvalidOperationException($"Field '{fieldName}' is constant in class '{this.FullName}'."); 157 | } 158 | 159 | try 160 | { 161 | var vTableMemorySize = this.Process.SizeOfPtr * this.VTableSize; 162 | var valuePtr = this.Process.ReadPtr(this.VTable + this.Process.MonoLibraryOffsets.VTable + vTableMemorySize); 163 | return field.GetValue(valuePtr); 164 | } 165 | catch (Exception e) 166 | { 167 | throw new Exception($"Exception received when trying to get static value for field '{fieldName}' in class '{this.FullName}': ${e.Message}.", e); 168 | } 169 | } 170 | 171 | public FieldDefinition GetField(string fieldName, string typeFullName = default) => 172 | this.fieldCache.GetOrAdd( 173 | (typeFullName, fieldName), 174 | k => this.Fields 175 | .FirstOrDefault( 176 | f => (f.Name == k.name) && ((k.@class == default) || (k.@class == f.DeclaringType.FullName)))); 177 | 178 | public void Init() 179 | { 180 | this.NestedIn?.Init(); 181 | this.Parent?.Init(); 182 | } 183 | 184 | private TypeDefinition GetClassDefinition(int address) => 185 | this.Image.GetTypeDefinition(this.ReadPtr(address)); 186 | 187 | private IReadOnlyList GetFields() 188 | { 189 | var firstField = this.ReadPtr(this.Image.Process.MonoLibraryOffsets.TypeDefinitionFields); 190 | if (firstField == IntPtr.Zero) 191 | { 192 | return this.Parent?.Fields ?? new List(); 193 | } 194 | 195 | var fields = new List(); 196 | if (this.ClassKind == MonoClassKind.GInst) 197 | { 198 | fields.AddRange(this.GetGeneric().GetFields()); 199 | } 200 | else 201 | { 202 | for (var fieldIndex = 0; fieldIndex < this.fieldCount; fieldIndex++) 203 | { 204 | var field = firstField + (fieldIndex * this.Process.MonoLibraryOffsets.TypeDefinitionFieldSize); 205 | if (this.Process.ReadPtr(field) == IntPtr.Zero) 206 | { 207 | break; 208 | } 209 | 210 | fields.Add(new FieldDefinition(this, field)); 211 | } 212 | } 213 | 214 | fields.AddRange(this.Parent?.Fields ?? new List()); 215 | 216 | return new ReadOnlyCollection(fields.OrderBy(f => f.Name).ToArray()); 217 | } 218 | 219 | private string GetFullName() 220 | { 221 | var builder = new StringBuilder(); 222 | var hierarchy = this.NestedHierarchy().Reverse().ToArray(); 223 | if (!string.IsNullOrWhiteSpace(this.NamespaceName)) 224 | { 225 | builder.Append($"{hierarchy[0].NamespaceName}."); 226 | } 227 | 228 | foreach (var definition in hierarchy) 229 | { 230 | builder.Append($"{definition.Name}+"); 231 | } 232 | 233 | return builder.ToString().TrimEnd('+'); 234 | } 235 | 236 | private IEnumerable NestedHierarchy() 237 | { 238 | yield return this; 239 | 240 | var nested = this.NestedIn; 241 | while (nested != default) 242 | { 243 | yield return nested; 244 | 245 | nested = nested.NestedIn; 246 | } 247 | } 248 | 249 | private TypeDefinition GetGeneric() 250 | { 251 | if (this.ClassKind != MonoClassKind.GInst) 252 | { 253 | return null; 254 | } 255 | 256 | var genericContainerPtr = this.ReadPtr(this.Process.MonoLibraryOffsets.TypeDefinitionMonoGenericClass); 257 | return this.Image.GetTypeDefinition(this.Process.ReadPtr(genericContainerPtr)); 258 | } 259 | } 260 | } -------------------------------------------------------------------------------- /src/HackF5.UnitySpy/ProcessFacade/ProcessFacade.cs: -------------------------------------------------------------------------------- 1 | namespace HackF5.UnitySpy.ProcessFacade 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Text; 6 | using HackF5.UnitySpy.Detail; 7 | using HackF5.UnitySpy.Util; 8 | using JetBrains.Annotations; 9 | using TypeCode = HackF5.UnitySpy.Detail.TypeCode; 10 | 11 | /// 12 | /// A facade over a process that provides access to its memory space. 13 | /// 14 | [PublicAPI] 15 | public abstract class ProcessFacade 16 | { 17 | public bool Is64Bits { get; set; } 18 | 19 | public int SizeOfPtr => this.Is64Bits ? 8 : 4; 20 | 21 | public string ReadAsciiString(IntPtr address, int maxSize = 1024) => 22 | this.ReadBufferValue(address, maxSize, b => b.ToAsciiString()); 23 | 24 | public string ReadAsciiStringPtr(IntPtr address, int maxSize = 1024) => 25 | this.ReadAsciiString(this.ReadPtr(address), maxSize); 26 | 27 | public int ReadInt32(IntPtr address) => 28 | this.ReadBufferValue(address, sizeof(int), b => b.ToInt32()); 29 | 30 | public long ReadInt64(IntPtr address) => 31 | this.ReadBufferValue(address, sizeof(long), b => b.ToInt64()); 32 | 33 | public object ReadManaged([NotNull] TypeInfo type, List genericTypeArguments, IntPtr address) 34 | { 35 | if (type == null) 36 | { 37 | throw new ArgumentNullException(nameof(type)); 38 | } 39 | 40 | switch (type.TypeCode) 41 | { 42 | case TypeCode.BOOLEAN: 43 | return this.ReadBufferValue(address, 1, b => b[0] != 0); 44 | 45 | case TypeCode.CHAR: 46 | return this.ReadBufferValue(address, sizeof(char), ConversionUtils.ToChar); 47 | 48 | case TypeCode.I1: 49 | return this.ReadBufferValue(address, sizeof(byte), b => b[0]); 50 | 51 | case TypeCode.U1: 52 | return this.ReadBufferValue(address, sizeof(sbyte), b => unchecked((sbyte)b[0])); 53 | 54 | case TypeCode.I2: 55 | return this.ReadBufferValue(address, sizeof(short), ConversionUtils.ToInt16); 56 | 57 | case TypeCode.U2: 58 | return this.ReadBufferValue(address, sizeof(ushort), ConversionUtils.ToUInt16); 59 | 60 | case TypeCode.I: 61 | case TypeCode.I4: 62 | return this.ReadInt32(address); 63 | 64 | case TypeCode.U: 65 | case TypeCode.U4: 66 | return this.ReadUInt32(address); 67 | 68 | case TypeCode.I8: 69 | return this.ReadInt64(address); 70 | // return this.ReadBufferValue(address, sizeof(char), ConversionUtils.ToInt64); 71 | 72 | case TypeCode.U8: 73 | return this.ReadUInt64(address); 74 | // return this.ReadBufferValue(address, sizeof(char), ConversionUtils.ToUInt64); 75 | 76 | case TypeCode.R4: 77 | return this.ReadBufferValue(address, sizeof(char), ConversionUtils.ToSingle); 78 | 79 | case TypeCode.R8: 80 | return this.ReadBufferValue(address, sizeof(char), ConversionUtils.ToDouble); 81 | 82 | case TypeCode.STRING: 83 | return this.ReadManagedString(address); 84 | 85 | case TypeCode.SZARRAY: 86 | return this.ReadManagedArray(type, genericTypeArguments, address); 87 | 88 | case TypeCode.VALUETYPE: 89 | try 90 | { 91 | return this.ReadManagedStructInstance(type, genericTypeArguments, address); 92 | } 93 | catch (Exception) 94 | { 95 | return this.ReadInt32(address); 96 | } 97 | 98 | case TypeCode.CLASS: 99 | return this.ReadManagedClassInstance(type, genericTypeArguments, address); 100 | 101 | case TypeCode.GENERICINST: 102 | return this.ReadManagedGenericObject(type, genericTypeArguments, address); 103 | 104 | //// this is the type code for generic structs class-internals.h_MonoGenericParam. Good luck with 105 | //// that! 106 | //// Using the Generic Object works in at least some cases, like 107 | //// when retrieving the NetCache service. 108 | //// It's probably better to have something incomplete here 109 | //// that will raise an exception later on than throwing the exception right away? 110 | case TypeCode.OBJECT: 111 | return this.ReadManagedGenericObject(type, genericTypeArguments, address); 112 | 113 | case TypeCode.VAR: 114 | // Really not sure this is the way to do it 115 | return this.ReadManagedVar(type, genericTypeArguments, address); 116 | 117 | // may need supporting 118 | case TypeCode.ARRAY: 119 | case TypeCode.ENUM: 120 | case TypeCode.MVAR: 121 | 122 | //// junk 123 | case TypeCode.END: 124 | case TypeCode.VOID: 125 | case TypeCode.PTR: 126 | case TypeCode.BYREF: 127 | case TypeCode.TYPEDBYREF: 128 | case TypeCode.FNPTR: 129 | case TypeCode.CMOD_REQD: 130 | case TypeCode.CMOD_OPT: 131 | case TypeCode.INTERNAL: 132 | case TypeCode.MODIFIER: 133 | case TypeCode.SENTINEL: 134 | case TypeCode.PINNED: 135 | throw new ArgumentException($"Cannot read values of type '{type.TypeCode}'."); 136 | 137 | default: 138 | throw new ArgumentOutOfRangeException( 139 | nameof(type), 140 | type, 141 | $"Cannot read unknown data type '{type.TypeCode}'."); 142 | } 143 | } 144 | 145 | public IntPtr ReadPtr(IntPtr address) => 146 | (IntPtr)(this.Is64Bits ? this.ReadUInt64(address) : this.ReadUInt32(address)); 147 | 148 | public uint ReadUInt32(IntPtr address) => 149 | this.ReadBufferValue(address, sizeof(uint), b => b.ToUInt32()); 150 | 151 | public ulong ReadUInt64(IntPtr address) => 152 | this.ReadBufferValue(address, sizeof(ulong), b => b.ToUInt64()); 153 | 154 | public byte ReadByte(IntPtr address) => 155 | this.ReadBufferValue(address, sizeof(byte), b => b.ToByte()); 156 | 157 | public byte[] ReadModule([NotNull] ModuleInfo moduleInfo) 158 | { 159 | if (moduleInfo == null) 160 | { 161 | throw new ArgumentNullException(nameof(moduleInfo)); 162 | } 163 | 164 | var buffer = new byte[moduleInfo.Size]; 165 | this.ReadProcessMemory(buffer, moduleInfo.BaseAddress); 166 | return buffer; 167 | } 168 | 169 | public abstract void ReadProcessMemory( 170 | byte[] buffer, 171 | IntPtr processAddress, 172 | bool allowPartialRead = false, 173 | int? size = default); 174 | 175 | public abstract ModuleInfo GetModule(string moduleName); 176 | 177 | private TValue ReadBufferValue(IntPtr address, int size, Func read) 178 | { 179 | var buffer = ByteArrayPool.Instance.Rent(size); 180 | 181 | try 182 | { 183 | this.ReadProcessMemory(buffer, address, size: size); 184 | return read(buffer); 185 | } 186 | finally 187 | { 188 | ByteArrayPool.Instance.Return(buffer); 189 | } 190 | } 191 | 192 | private object[] ReadManagedArray(TypeInfo type, List genericTypeArguments, IntPtr address) 193 | { 194 | var ptr = this.ReadPtr(address); 195 | if (ptr == IntPtr.Zero) 196 | { 197 | return default; 198 | } 199 | 200 | var vtable = this.ReadPtr(ptr); 201 | var arrayDefinitionPtr = this.ReadPtr(vtable); 202 | var arrayDefinition = type.Image.GetTypeDefinition(arrayDefinitionPtr); 203 | var elementDefinition = type.Image.GetTypeDefinition(this.ReadPtr(arrayDefinitionPtr)); 204 | 205 | var count = this.ReadInt32(ptr + (this.SizeOfPtr * 3)); 206 | var start = ptr + (this.SizeOfPtr * 4); 207 | var result = new object[count]; 208 | for (var i = 0; i < count; i++) 209 | { 210 | result[i] = elementDefinition.TypeInfo.GetValue(genericTypeArguments, start + (i * arrayDefinition.Size)); 211 | } 212 | 213 | return result; 214 | } 215 | 216 | private ManagedClassInstance ReadManagedClassInstance(TypeInfo type, List genericTypeArguments, IntPtr address) 217 | { 218 | var ptr = this.ReadPtr(address); 219 | return ptr == IntPtr.Zero 220 | ? default 221 | : new ManagedClassInstance(type.Image, genericTypeArguments, ptr); 222 | } 223 | 224 | private object ReadManagedGenericObject(TypeInfo type, List genericTypeArguments, IntPtr address) 225 | { 226 | // TODO check if this is correct because we are getting the wrong class instance for GENERICINST 227 | var genericDefinition = type.Image.GetTypeDefinition(this.ReadPtr(type.Data)); 228 | 229 | if (genericDefinition.IsValueType) 230 | { 231 | return new ManagedStructInstance(genericDefinition, genericTypeArguments, address); 232 | } 233 | 234 | return this.ReadManagedClassInstance(type, genericTypeArguments, address); 235 | } 236 | 237 | private object ReadManagedVar(TypeInfo type, List genericTypeArguments, IntPtr address) 238 | { 239 | var monoGenericParamPtr = type.Data; 240 | int numberOfGenericArgument = this.ReadInt32(monoGenericParamPtr + this.SizeOfPtr); 241 | 242 | int offset = 0; 243 | for (int i = 0; i < numberOfGenericArgument; i++) 244 | { 245 | offset += this.GetSize(genericTypeArguments[i].TypeCode) - this.SizeOfPtr; 246 | } 247 | 248 | var genericArgumentType = genericTypeArguments[numberOfGenericArgument]; 249 | return this.ReadManaged(genericArgumentType, null, address + offset); 250 | } 251 | 252 | private string ReadManagedString(IntPtr address) 253 | { 254 | var ptr = this.ReadPtr(address); 255 | if (ptr == IntPtr.Zero) 256 | { 257 | return default; 258 | } 259 | 260 | // Offsets taken from: 261 | // struct _MonoString { 262 | // MonoObject object; // Has two pointers (SizeOfPtr * 2) 263 | // int32_t length; 264 | // mono_unichar2 chars [MONO_ZERO_LEN_ARRAY]; 265 | // }; 266 | var length = this.ReadInt32(ptr + (this.SizeOfPtr * 2)); 267 | 268 | return this.ReadBufferValue( 269 | ptr + (this.SizeOfPtr * 2) + 4, 270 | 2 * length, 271 | b => Encoding.Unicode.GetString(b, 0, 2 * length)); 272 | } 273 | 274 | private object ReadManagedStructInstance(TypeInfo type, List genericTypeArguments, IntPtr address) 275 | { 276 | var definition = type.Image.GetTypeDefinition(type.Data); 277 | var obj = new ManagedStructInstance(definition, genericTypeArguments, address); 278 | 279 | // var t = obj.GetValue("enumSeperator"); 280 | return obj.TypeDefinition.IsEnum ? obj.GetValue("value__") : obj; 281 | } 282 | 283 | private int GetSize(TypeCode typeCode) 284 | { 285 | switch (typeCode) 286 | { 287 | case TypeCode.BOOLEAN: 288 | return sizeof(bool); 289 | 290 | case TypeCode.CHAR: 291 | return sizeof(char); 292 | 293 | case TypeCode.I1: 294 | return sizeof(byte); 295 | 296 | case TypeCode.U1: 297 | return sizeof(sbyte); 298 | 299 | case TypeCode.I2: 300 | return sizeof(short); 301 | 302 | case TypeCode.U2: 303 | return sizeof(ushort); 304 | 305 | case TypeCode.I: 306 | case TypeCode.I4: 307 | return sizeof(int); 308 | 309 | case TypeCode.U: 310 | case TypeCode.U4: 311 | return sizeof(uint); 312 | 313 | case TypeCode.I8: 314 | return sizeof(long); 315 | 316 | case TypeCode.U8: 317 | return sizeof(ulong); 318 | 319 | case TypeCode.R4: 320 | case TypeCode.R8: 321 | return sizeof(char); 322 | 323 | case TypeCode.STRING: 324 | case TypeCode.SZARRAY: 325 | case TypeCode.VALUETYPE: 326 | case TypeCode.CLASS: 327 | case TypeCode.GENERICINST: 328 | case TypeCode.OBJECT: 329 | case TypeCode.VAR: 330 | return this.SizeOfPtr; 331 | 332 | // may need supporting 333 | case TypeCode.ARRAY: 334 | case TypeCode.ENUM: 335 | case TypeCode.MVAR: 336 | 337 | //// junk 338 | case TypeCode.END: 339 | case TypeCode.VOID: 340 | case TypeCode.PTR: 341 | case TypeCode.BYREF: 342 | case TypeCode.TYPEDBYREF: 343 | case TypeCode.FNPTR: 344 | case TypeCode.CMOD_REQD: 345 | case TypeCode.CMOD_OPT: 346 | case TypeCode.INTERNAL: 347 | case TypeCode.MODIFIER: 348 | case TypeCode.SENTINEL: 349 | case TypeCode.PINNED: 350 | throw new ArgumentException($"Cannot get size of types '{typeCode}'."); 351 | 352 | default: 353 | throw new ArgumentOutOfRangeException( 354 | nameof(typeCode), 355 | typeCode, 356 | $"Cannot get size of unknown data type '{typeCode}'."); 357 | } 358 | } 359 | } 360 | } -------------------------------------------------------------------------------- /src/HackF5.UnitySpy/Offsets/MonoLibraryOffsets.cs: -------------------------------------------------------------------------------- 1 | // ReSharper disable IdentifierTypo 2 | namespace HackF5.UnitySpy.Offsets 3 | { 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Diagnostics; 7 | using System.IO; 8 | using System.Runtime.InteropServices; 9 | 10 | public class MonoLibraryOffsets 11 | { 12 | public static readonly MonoLibraryOffsets Unity2018_4_10_x86_PE_Offsets = new MonoLibraryOffsets 13 | { 14 | UnityVersions = new List { UnityVersion.Version2018_4_10 }, 15 | Is64Bits = false, 16 | Format = BinaryFormat.PE, 17 | MonoLibrary = "mono-2.0-bdwgc.dll", 18 | 19 | AssemblyImage = 0x44, 20 | ReferencedAssemblies = 0x6c, 21 | ImageClassCache = 0x354, 22 | HashTableSize = 0xc, 23 | HashTableTable = 0x14, 24 | 25 | TypeDefinitionFieldSize = 0x10, 26 | TypeDefinitionBitFields = 0x14, 27 | TypeDefinitionClassKind = 0x1e, 28 | TypeDefinitionParent = 0x20, 29 | TypeDefinitionNestedIn = 0x24, 30 | TypeDefinitionName = 0x2c, 31 | TypeDefinitionNamespace = 0x30, 32 | TypeDefinitionVTableSize = 0x38, 33 | TypeDefinitionSize = 0x5c, 34 | TypeDefinitionFields = 0x60, 35 | TypeDefinitionByValArg = 0x74, 36 | TypeDefinitionRuntimeInfo = 0x84, 37 | 38 | TypeDefinitionFieldCount = 0xa4, 39 | TypeDefinitionNextClassCache = 0xa8, 40 | 41 | TypeDefinitionGenericContainer = 0xac, 42 | TypeDefinitionMonoGenericClass = 0x94, 43 | 44 | TypeDefinitionRuntimeInfoDomainVTables = 0x4, 45 | 46 | VTable = 0x28, 47 | }; 48 | 49 | public static readonly MonoLibraryOffsets Unity2019_4_2020_3_x64_PE_Offsets = new MonoLibraryOffsets 50 | { 51 | UnityVersions = new List { UnityVersion.Version2019_4_5, UnityVersion.Version2020_3_13 }, 52 | Is64Bits = true, 53 | Format = BinaryFormat.PE, 54 | MonoLibrary = "mono-2.0-bdwgc.dll", 55 | 56 | AssemblyImage = 0x44 + 0x1c, 57 | ReferencedAssemblies = 0x6c + 0x5c, 58 | ImageClassCache = 0x354 + 0x16c, 59 | HashTableSize = 0xc + 0xc, 60 | HashTableTable = 0x14 + 0xc, 61 | 62 | TypeDefinitionFieldSize = 0x10 + 0x10, 63 | TypeDefinitionBitFields = 0x14 + 0xc, 64 | TypeDefinitionClassKind = 0x1e + 0xc, 65 | TypeDefinitionParent = 0x20 + 0x10, // 0x30 66 | TypeDefinitionNestedIn = 0x24 + 0x14, // 0x38 67 | TypeDefinitionName = 0x2c + 0x1c, // 0x48 68 | TypeDefinitionNamespace = 0x30 + 0x20, // 0x50 69 | TypeDefinitionVTableSize = 0x38 + 0x24, 70 | TypeDefinitionSize = 0x5c + 0x20 + 0x18 - 0x4, // 0x90 Array Element Count 71 | TypeDefinitionFields = 0x60 + 0x20 + 0x18, // 0x98 72 | TypeDefinitionByValArg = 0x74 + 0x44, 73 | TypeDefinitionRuntimeInfo = 0x84 + 0x34 + 0x18, // 0xD0 74 | 75 | TypeDefinitionFieldCount = 0xa4 + 0x34 + 0x10 + 0x18, 76 | TypeDefinitionNextClassCache = 0xa8 + 0x34 + 0x10 + 0x18 + 0x4, 77 | 78 | TypeDefinitionMonoGenericClass = 0x94 + 0x34 + 0x18 + 0x10, 79 | TypeDefinitionGenericContainer = 0x110, 80 | 81 | TypeDefinitionRuntimeInfoDomainVTables = 0x4 + 0x4, 82 | 83 | VTable = 0x28 + 0x18, 84 | }; 85 | 86 | public static readonly MonoLibraryOffsets Unity2021_3_2022_3_x64_PE_Offsets = new MonoLibraryOffsets 87 | { 88 | UnityVersions = new List() { UnityVersion.Version2021_3_14, UnityVersion.Version2022_3_42 }, 89 | Is64Bits = true, 90 | Format = BinaryFormat.PE, 91 | MonoLibrary = "mono-2.0-bdwgc.dll", 92 | 93 | // offset in _MonoAssembly to field 'image' (Type MonoImage*) 94 | AssemblyImage = 0x10 + 0x50, 95 | 96 | // field 'domain_assemblies' in _MonoDomain (domain-internals.h) 97 | ReferencedAssemblies = 0xa0, 98 | 99 | // field 'class_cache' in _MonoImage 100 | ImageClassCache = 0x4d0, 101 | HashTableSize = 0xc + 0xc, 102 | HashTableTable = 0x14 + 0xc, 103 | 104 | // size of every field in the field information | source _MonoClassField (class-internals.h) 105 | TypeDefinitionFieldSize = 0x20, // 3 ptr + int + padding 106 | 107 | // _MonoClass 108 | // starting from size_inited, valuetype, enumtype 109 | TypeDefinitionBitFields = 0x14 + 0xc, 110 | // class_kind 111 | TypeDefinitionClassKind = 0x1b, // alt: 0x1e + 0xc 112 | // parent 113 | TypeDefinitionParent = 0x30, 114 | // nested_in 115 | TypeDefinitionNestedIn = 0x38, 116 | // name 117 | TypeDefinitionName = 0x48, 118 | // name_space 119 | TypeDefinitionNamespace = 0x50, // 0x48 + 0x8 120 | // vtable_size 121 | TypeDefinitionVTableSize = 0x5C, // 0x50 + 0x8 + 0x4 122 | // sizes 123 | TypeDefinitionSize = 0x90, // Static Fields / Array Element Count / Generic Param Types 124 | // fields 125 | TypeDefinitionFields = 0x98, // 0x98 126 | // _byval_arg 127 | TypeDefinitionByValArg = 0xB8, // 0x98 + 0x10 (2 ptr) + 0x10 (sizeof(MonoType)) 128 | // runtime_info 129 | TypeDefinitionRuntimeInfo = 0x84 + 0x34 + 0x18, // 0xD0 130 | 131 | // MonoClassDef 132 | // field_count 133 | TypeDefinitionFieldCount = 0xa4 + 0x34 + 0x18 + 0x10, // 0xE0 134 | // next_class_cache 135 | TypeDefinitionNextClassCache = 0xa8 + 0x34 + 0x18 + 0x10 + 0x4, // 0xE4 136 | 137 | TypeDefinitionMonoGenericClass = 0x94 + 0x34 + 0x18 + 0x10, 138 | TypeDefinitionGenericContainer = 0x110, 139 | 140 | TypeDefinitionRuntimeInfoDomainVTables = 0x2 + 0x6, // 2 byte 'max_domain' + alignment to pointer size 141 | 142 | // MonoVTable.vtable 143 | // 5 ptr + 8 byte (max_interface_id -> gc_bits) + 8 bytes (4 + 4 padding) + 2 ptr 144 | // 0x28 + 0x8 + 0x8 + 0x10 145 | VTable = 0x48, 146 | }; 147 | 148 | public static readonly MonoLibraryOffsets Unity2019_4_2020_3_x64_MachO_Offsets = new MonoLibraryOffsets 149 | { 150 | UnityVersions = new List { UnityVersion.Version2019_4_5 }, 151 | Is64Bits = true, 152 | Format = BinaryFormat.MachO, 153 | MonoLibrary = "libmonobdwgc-2.0.dylib", 154 | 155 | AssemblyImage = 0x44 + 0x1c, 156 | ReferencedAssemblies = 0x6c + 0x5c + 0x18, 157 | ImageClassCache = 0x354 + 0x16c, 158 | HashTableSize = 0xc + 0xc, 159 | HashTableTable = 0x14 + 0xc, 160 | 161 | TypeDefinitionFieldSize = 0x10 + 0x10, 162 | TypeDefinitionBitFields = 0x14 + 0xc, 163 | TypeDefinitionClassKind = 0x1e + 0xc - 0x6, 164 | TypeDefinitionParent = 0x20 + 0x10 - 0x8, // 0x28 165 | TypeDefinitionNestedIn = 0x24 + 0x14 - 0x8, // 0x30 166 | TypeDefinitionName = 0x2c + 0x1c - 0x8, // 0x40 167 | TypeDefinitionNamespace = 0x30 + 0x20 - 0x8, // 0x48 168 | TypeDefinitionVTableSize = 0x38 + 0x24 - 0x8, 169 | TypeDefinitionSize = 0x5c + 0x20 + 0x18 - 0x4 - 0x8, // 0x88 Array Element Count 170 | TypeDefinitionFields = 0x60 + 0x20 + 0x18 - 0x8, // 0x90 171 | TypeDefinitionByValArg = 0x74 + 0x44 - 0x8, 172 | TypeDefinitionRuntimeInfo = 0x84 + 0x34 + 0x18 - 0x8, // 0xD0 173 | 174 | TypeDefinitionFieldCount = 0xa4 + 0x34 + 0x10 + 0x18 - 0x8, 175 | TypeDefinitionNextClassCache = 0xa8 + 0x34 + 0x10 + 0x18 + 0x4 - 0x8, 176 | 177 | TypeDefinitionMonoGenericClass = 0x94 + 0x34 + 0x18 + 0x10 - 0x8, 178 | TypeDefinitionGenericContainer = 0x110 - 0x8, 179 | 180 | TypeDefinitionRuntimeInfoDomainVTables = 0x4 + 0x4, 181 | 182 | VTable = 0x28 + 0x18, 183 | }; 184 | 185 | private static readonly List SupportedVersions = new List() 186 | { 187 | Unity2018_4_10_x86_PE_Offsets, 188 | Unity2019_4_2020_3_x64_PE_Offsets, 189 | Unity2019_4_2020_3_x64_MachO_Offsets, 190 | Unity2021_3_2022_3_x64_PE_Offsets, 191 | }; 192 | 193 | public List UnityVersions { get; private set; } 194 | 195 | public bool Is64Bits { get; private set; } 196 | 197 | public BinaryFormat Format { get; private set; } 198 | 199 | public string MonoLibrary { get; private set; } 200 | 201 | public int AssemblyImage { get; private set; } 202 | 203 | public int ReferencedAssemblies { get; private set; } 204 | 205 | public int ImageClassCache { get; private set; } 206 | 207 | public int HashTableSize { get; private set; } 208 | 209 | public int HashTableTable { get; private set; } 210 | 211 | // MonoClass Offsets 212 | public int TypeDefinitionFieldSize { get; private set; } 213 | 214 | public int TypeDefinitionBitFields { get; private set; } 215 | 216 | public int TypeDefinitionClassKind { get; private set; } 217 | 218 | public int TypeDefinitionParent { get; private set; } 219 | 220 | public int TypeDefinitionNestedIn { get; private set; } 221 | 222 | public int TypeDefinitionName { get; private set; } 223 | 224 | public int TypeDefinitionNamespace { get; private set; } 225 | 226 | public int TypeDefinitionVTableSize { get; private set; } 227 | 228 | public int TypeDefinitionSize { get; private set; } 229 | 230 | public int TypeDefinitionFields { get; private set; } 231 | 232 | public int TypeDefinitionByValArg { get; private set; } 233 | 234 | public int TypeDefinitionRuntimeInfo { get; private set; } 235 | 236 | // MonoClassDef Offsets 237 | public int TypeDefinitionFieldCount { get; private set; } 238 | 239 | public int TypeDefinitionNextClassCache { get; private set; } 240 | 241 | // MonoClassGtd Offsets 242 | public int TypeDefinitionGenericContainer { get; private set; } 243 | 244 | // MonoClassGenericInst Offsets 245 | public int TypeDefinitionMonoGenericClass { get; private set; } 246 | 247 | // MonoClassRuntimeInfo Offsets 248 | public int TypeDefinitionRuntimeInfoDomainVTables { get; private set; } 249 | 250 | // MonoVTable Offsets 251 | public int VTable { get; private set; } 252 | 253 | public static MonoLibraryOffsets GetOffsets(string gameExecutableFilePath, bool force = true) 254 | { 255 | if (gameExecutableFilePath == null) 256 | { 257 | throw new ArgumentNullException("gameExecutableFilePath parameter cannot be null"); 258 | } 259 | 260 | string unityVersion; 261 | if (gameExecutableFilePath.EndsWith(".exe")) 262 | { 263 | var peHeader = new PeNet.PeFile(gameExecutableFilePath); 264 | unityVersion = peHeader.Resources.VsVersionInfo.StringFileInfo.StringTable[0].FileVersion; 265 | 266 | // Taken from here https://stackoverflow.com/questions/1001404/check-if-unmanaged-dll-is-32-bit-or-64-bit; 267 | // See http://www.microsoft.com/whdc/system/platform/firmware/PECOFF.mspx 268 | // Offset to PE header is always at 0x3C. 269 | // The PE header starts with "PE\0\0" = 0x50 0x45 0x00 0x00, 270 | // followed by a 2-byte machine type field (see the document above for the enum). 271 | FileStream fs = new FileStream(gameExecutableFilePath, FileMode.Open, FileAccess.Read); 272 | BinaryReader br = new BinaryReader(fs); 273 | fs.Seek(0x3c, SeekOrigin.Begin); 274 | int peOffset = br.ReadInt32(); 275 | fs.Seek(peOffset, SeekOrigin.Begin); 276 | uint peHead = br.ReadUInt32(); 277 | 278 | // "PE\0\0", little-endian 279 | if (peHead != 0x00004550) 280 | { 281 | throw new Exception("Can't find PE header"); 282 | } 283 | 284 | int machineType = br.ReadUInt16(); 285 | br.Close(); 286 | fs.Close(); 287 | 288 | Console.WriteLine($"Game executable file closed, unity version = {unityVersion}."); 289 | 290 | switch (machineType) 291 | { 292 | case 0x8664: // IMAGE_FILE_MACHINE_AMD64 293 | return GetOffsets(unityVersion, true, BinaryFormat.PE, force); 294 | case 0x14c: // IMAGE_FILE_MACHINE_I386 295 | return GetOffsets(unityVersion, false, BinaryFormat.PE, force); 296 | } 297 | } 298 | else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) 299 | { 300 | FileInfo gameExecutableFile = new FileInfo(gameExecutableFilePath); 301 | string infoPlist = File.ReadAllText(gameExecutableFile.Directory.Parent.FullName + "/Info.plist"); 302 | string unityVersionField = "Unity Player version "; 303 | int indexOfUnityVersionField = infoPlist.IndexOf(unityVersionField); 304 | unityVersion = infoPlist.Substring(indexOfUnityVersionField + unityVersionField.Length).Split(' ')[0]; 305 | 306 | // Start the child process. 307 | Process p = new Process(); 308 | 309 | // Redirect the output stream of the child process. 310 | p.StartInfo.UseShellExecute = false; 311 | p.StartInfo.RedirectStandardOutput = true; 312 | p.StartInfo.FileName = "file"; 313 | p.StartInfo.Arguments = gameExecutableFile.FullName; 314 | p.Start(); 315 | 316 | // Do not wait for the child process to exit before 317 | // reading to the end of its redirected stream. 318 | // p.WaitForExit(); 319 | // Read the output stream first and then wait. 320 | string output = p.StandardOutput.ReadToEnd(); 321 | p.WaitForExit(); 322 | return GetOffsets(unityVersion, output.EndsWith("x86_64\n"), BinaryFormat.MachO, force); 323 | } 324 | 325 | throw new NotSupportedException("Platform not supported"); 326 | } 327 | 328 | public static MonoLibraryOffsets GetOffsets(string unityVersion, bool is64Bits, BinaryFormat format, bool force = true) 329 | { 330 | return GetOffsets(UnityVersion.Parse(unityVersion), is64Bits, format, force); 331 | } 332 | 333 | private static MonoLibraryOffsets GetOffsets(UnityVersion unityVersion, bool is64Bits, BinaryFormat format, bool force = true) 334 | { 335 | MonoLibraryOffsets monoLibraryOffsets = SupportedVersions.Find( 336 | offsets => offsets.Is64Bits == is64Bits && 337 | offsets.Format == format && 338 | offsets.UnityVersions.Contains(unityVersion)); 339 | 340 | UnityVersion selectedUnityVersion = unityVersion; 341 | 342 | if (monoLibraryOffsets == null) 343 | { 344 | string mode = is64Bits ? "in 64 bits mode" : "in 32 bits mode"; 345 | string unsupportedMsg = $"The unity version the process is running " + 346 | $"({unityVersion} {mode}) is not supported."; 347 | if (force) 348 | { 349 | List matchingArchitectureSupportedVersion = SupportedVersions.FindAll(v => 350 | v.Is64Bits == is64Bits && v.Format == format); 351 | 352 | if (matchingArchitectureSupportedVersion.Count > 0) 353 | { 354 | MonoLibraryOffsets bestCandidate = matchingArchitectureSupportedVersion[0]; 355 | UnityVersion bestCandidateUnityVersion = GetBestCandidate(unityVersion, bestCandidate.UnityVersions); 356 | 357 | if (matchingArchitectureSupportedVersion.Count > 1) 358 | { 359 | int bestCandidateYearDistance = 360 | Math.Abs(unityVersion.Year - bestCandidateUnityVersion.Year); 361 | int bestCandidateVersionWithinYearDistance = 362 | Math.Abs(unityVersion.VersionWithinYear - bestCandidateUnityVersion.VersionWithinYear); 363 | int bestCandidateSubversionWithinYearDistance = 364 | Math.Abs(unityVersion.SubversionWithinYear - bestCandidateUnityVersion.SubversionWithinYear); 365 | for (int i = 1; i < matchingArchitectureSupportedVersion.Count; i++) 366 | { 367 | UnityVersion candidateVersion = GetBestCandidate(unityVersion, matchingArchitectureSupportedVersion[i].UnityVersions); 368 | if ( 369 | Math.Abs(unityVersion.Year - candidateVersion.Year) < bestCandidateYearDistance 370 | || ( 371 | Math.Abs(unityVersion.Year - candidateVersion.Year) == bestCandidateYearDistance 372 | && ( 373 | Math.Abs(unityVersion.VersionWithinYear - candidateVersion.VersionWithinYear) 374 | < bestCandidateVersionWithinYearDistance 375 | || ( 376 | Math.Abs(unityVersion.VersionWithinYear - candidateVersion.VersionWithinYear) 377 | == bestCandidateVersionWithinYearDistance 378 | && ( 379 | Math.Abs(unityVersion.SubversionWithinYear - candidateVersion.SubversionWithinYear) 380 | < bestCandidateSubversionWithinYearDistance 381 | ) 382 | ) 383 | ) 384 | ) 385 | ) 386 | { 387 | bestCandidate = matchingArchitectureSupportedVersion[i]; 388 | bestCandidateYearDistance = 389 | Math.Abs(unityVersion.Year - candidateVersion.Year); 390 | bestCandidateVersionWithinYearDistance = 391 | Math.Abs(unityVersion.VersionWithinYear - candidateVersion.VersionWithinYear); 392 | bestCandidateSubversionWithinYearDistance = 393 | Math.Abs(unityVersion.SubversionWithinYear - candidateVersion.SubversionWithinYear); 394 | } 395 | } 396 | } 397 | 398 | Console.WriteLine(unsupportedMsg); 399 | Console.WriteLine($"Offsets of {bestCandidateUnityVersion} selected instead."); 400 | 401 | return bestCandidate; 402 | } 403 | } 404 | 405 | throw new NotSupportedException(unsupportedMsg); 406 | } 407 | 408 | return monoLibraryOffsets; 409 | } 410 | 411 | private static UnityVersion GetBestCandidate(UnityVersion target, List unityVersions) 412 | { 413 | if (unityVersions == null || unityVersions.Count == 0) 414 | { 415 | throw new ArgumentException("UnityVersions must contain at least one element"); 416 | } 417 | 418 | UnityVersion bestCandidate = unityVersions[0]; 419 | if (unityVersions.Count > 1) 420 | { 421 | int bestCandidateYearDistance = 422 | Math.Abs(target.Year - bestCandidate.Year); 423 | int bestCandidateVersionWithinYearDistance = 424 | Math.Abs(target.VersionWithinYear - bestCandidate.VersionWithinYear); 425 | int bestCandidateSubversionWithinYearDistance = 426 | Math.Abs(target.SubversionWithinYear - bestCandidate.SubversionWithinYear); 427 | for (int i = 1; i < unityVersions.Count; i++) 428 | { 429 | UnityVersion candidateVersion = unityVersions[i]; 430 | if ( 431 | Math.Abs(target.Year - candidateVersion.Year) < bestCandidateYearDistance 432 | || ( 433 | Math.Abs(target.Year - candidateVersion.Year) == bestCandidateYearDistance 434 | && ( 435 | Math.Abs(target.VersionWithinYear - candidateVersion.VersionWithinYear) 436 | < bestCandidateVersionWithinYearDistance 437 | || ( 438 | Math.Abs(target.VersionWithinYear - candidateVersion.VersionWithinYear) 439 | == bestCandidateVersionWithinYearDistance 440 | && ( 441 | Math.Abs(target.SubversionWithinYear - candidateVersion.SubversionWithinYear) 442 | < bestCandidateSubversionWithinYearDistance 443 | ) 444 | ) 445 | ) 446 | ) 447 | ) 448 | { 449 | bestCandidate = unityVersions[i]; 450 | bestCandidateYearDistance = 451 | Math.Abs(target.Year - candidateVersion.Year); 452 | bestCandidateVersionWithinYearDistance = 453 | Math.Abs(target.VersionWithinYear - candidateVersion.VersionWithinYear); 454 | bestCandidateSubversionWithinYearDistance = 455 | Math.Abs(target.SubversionWithinYear - candidateVersion.SubversionWithinYear); 456 | } 457 | } 458 | } 459 | 460 | return bestCandidate; 461 | } 462 | } 463 | } -------------------------------------------------------------------------------- /src/mtga-tracker-daemon/HttpServer.cs: -------------------------------------------------------------------------------- 1 | // Based on the work of Benjamin N. Summerton on HttpServer.cs 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Diagnostics; 5 | using System.IO; 6 | using System.Text; 7 | using System.Net; 8 | using System.Net.Http; 9 | using System.Reflection; 10 | using System.Runtime.InteropServices; 11 | using System.Threading.Tasks; 12 | 13 | using HackF5.UnitySpy; 14 | using HackF5.UnitySpy.Detail; 15 | using HackF5.UnitySpy.Offsets; 16 | using HackF5.UnitySpy.ProcessFacade; 17 | 18 | using ICSharpCode.SharpZipLib.GZip; 19 | using ICSharpCode.SharpZipLib.Tar; 20 | 21 | using Newtonsoft.Json; 22 | using Microsoft.Data.Sqlite; 23 | using MTGATrackerDaemon.Models; 24 | 25 | namespace MTGATrackerDaemon 26 | { 27 | public class HttpServer 28 | { 29 | private HttpListener listener; 30 | 31 | private bool runServer = true; 32 | 33 | private Version currentVersion; 34 | 35 | private bool updating = false; 36 | 37 | public void Start(string url) 38 | { 39 | var assembly = Assembly.GetExecutingAssembly(); 40 | currentVersion = assembly.GetName().Version; 41 | Console.WriteLine($"Current version = {currentVersion}"); 42 | 43 | CheckForUpdates(); 44 | 45 | // Create a Http server and start listening for incoming connections 46 | listener = new HttpListener(); 47 | listener.Prefixes.Add(url); 48 | listener.Start(); 49 | Console.WriteLine("Listening for connections on {0}", url); 50 | 51 | // Handle requests 52 | Task listenTask = HandleIncomingConnections(); 53 | listenTask.GetAwaiter().GetResult(); 54 | 55 | // Close the listener 56 | listener.Close(); 57 | } 58 | 59 | private async Task HandleIncomingConnections() 60 | { 61 | // While a user hasn't visited the `shutdown` url, keep on handling requests 62 | while (runServer) 63 | { 64 | try 65 | { 66 | // Will wait here until we hear from a connection 67 | HttpListenerContext ctx = await listener.GetContextAsync(); 68 | 69 | // Peel out the requests and response objects 70 | HttpListenerRequest request = ctx.Request; 71 | if(request.IsLocal) 72 | { 73 | await HandleRequest(request, ctx.Response); 74 | } 75 | } 76 | catch (Exception) 77 | { 78 | 79 | } 80 | } 81 | } 82 | 83 | private async Task HandleRequest(HttpListenerRequest request, HttpListenerResponse response) { 84 | string responseJSON = "{\"error\":\"unsupported request\"}"; 85 | 86 | // If `shutdown` url requested w/ POST, then shutdown the server after serving the page 87 | if (request.HttpMethod == "POST") 88 | { 89 | if (request.Url.AbsolutePath == "/shutdown") 90 | { 91 | Console.WriteLine("Shutdown requested"); 92 | var shutdownResponse = new ShutdownResponseData { Result = "shutdown request accepted" }; 93 | responseJSON = JsonConvert.SerializeObject(shutdownResponse); 94 | runServer = false; 95 | } 96 | else if(request.Url.AbsolutePath == "/checkForUpdates") 97 | { 98 | responseJSON = JsonConvert.SerializeObject(CheckForUpdates()); 99 | } 100 | } 101 | else if (request.HttpMethod == "GET") 102 | { 103 | if (request.Url.AbsolutePath == "/status") 104 | { 105 | Process mtgaProcess = GetMTGAProcess(); 106 | var statusResponse = new StatusResponseData 107 | { 108 | IsRunning = mtgaProcess != null, 109 | DaemonVersion = currentVersion.ToString(), 110 | Updating = updating, 111 | ProcessId = mtgaProcess?.Id ?? -1 112 | }; 113 | responseJSON = JsonConvert.SerializeObject(statusResponse); 114 | } 115 | else if (request.Url.AbsolutePath == "/cards") 116 | { 117 | try 118 | { 119 | DateTime startTime = DateTime.Now; 120 | IAssemblyImage assemblyImage = CreateAssemblyImage(); 121 | object[] cards = assemblyImage["WrapperController"]["k__BackingField"]["k__BackingField"]["_inventoryServiceWrapper"]["k__BackingField"]["_entries"]; 122 | 123 | var cardList = new List(); 124 | for (int i = 0; i < cards.Length; i++) 125 | { 126 | if(cards[i] is ManagedStructInstance cardInstance) 127 | { 128 | int owned = cardInstance.GetValue("value"); 129 | if (owned > 0) 130 | { 131 | uint groupId = cardInstance.GetValue("key"); 132 | cardList.Add(new CardOwnership { GrpId = groupId, Owned = owned }); 133 | } 134 | } 135 | } 136 | 137 | TimeSpan ts = (DateTime.Now - startTime); 138 | var cardsResponse = new CardsResponseData 139 | { 140 | Cards = cardList.ToArray(), 141 | ElapsedTime = (int)ts.TotalMilliseconds 142 | }; 143 | responseJSON = JsonConvert.SerializeObject(cardsResponse); 144 | } 145 | catch (Exception ex) 146 | { 147 | var errorResponse = new ErrorResponseData { Error = ex.ToString() }; 148 | responseJSON = JsonConvert.SerializeObject(errorResponse); 149 | } 150 | } 151 | else if (request.Url.AbsolutePath == "/playerId") 152 | { 153 | try 154 | { 155 | DateTime startTime = DateTime.Now; 156 | IAssemblyImage assemblyImage = CreateAssemblyImage(); 157 | ManagedClassInstance accountInfo = (ManagedClassInstance) assemblyImage["WrapperController"]["k__BackingField"]["k__BackingField"]["k__BackingField"]; 158 | 159 | string playerID = accountInfo.GetValue("AccountID"); 160 | string displayName = accountInfo.GetValue("DisplayName"); 161 | string personaID = accountInfo.GetValue("PersonaID"); 162 | TimeSpan ts = (DateTime.Now - startTime); 163 | 164 | var playerResponse = new PlayerIDResponseData 165 | { 166 | PlayerID = playerID, 167 | DisplayName = displayName, 168 | PersonaID = personaID, 169 | ElapsedTime = (int)ts.TotalMilliseconds 170 | }; 171 | responseJSON = JsonConvert.SerializeObject(playerResponse); 172 | } 173 | catch (Exception ex) 174 | { 175 | var errorResponse = new ErrorResponseData { Error = ex.ToString() }; 176 | responseJSON = JsonConvert.SerializeObject(errorResponse); 177 | } 178 | } 179 | else if (request.Url.AbsolutePath == "/inventory") 180 | { 181 | try 182 | { 183 | DateTime startTime = DateTime.Now; 184 | IAssemblyImage assemblyImage = CreateAssemblyImage(); 185 | var inventory = assemblyImage["WrapperController"]["k__BackingField"]["k__BackingField"]["_inventoryServiceWrapper"]["m_inventory"]; 186 | 187 | TimeSpan ts = (DateTime.Now - startTime); 188 | var inventoryResponse = new InventoryResponseData 189 | { 190 | Gems = inventory["gems"], 191 | Gold = inventory["gold"], 192 | ElapsedTime = (int)ts.TotalMilliseconds 193 | }; 194 | responseJSON = JsonConvert.SerializeObject(inventoryResponse); 195 | } 196 | catch (Exception ex) 197 | { 198 | var errorResponse = new ErrorResponseData { Error = ex.ToString() }; 199 | responseJSON = JsonConvert.SerializeObject(errorResponse); 200 | } 201 | } 202 | else if (request.Url.AbsolutePath == "/events") 203 | { 204 | try 205 | { 206 | DateTime startTime = DateTime.Now; 207 | IAssemblyImage assemblyImage = CreateAssemblyImage(); 208 | object[] events = assemblyImage["PAPA"]["_instance"]["_eventManager"]["_eventsServiceWrapper"]["_cachedEvents"]["_items"]; 209 | 210 | var eventList = new List(); 211 | for (int i = 0; i < events.Length; i++) 212 | { 213 | if(events[i] is ManagedClassInstance eventInstance) 214 | { 215 | string eventId = eventInstance.GetValue("InternalEventName"); 216 | eventList.Add(eventId); 217 | } 218 | } 219 | 220 | TimeSpan ts = (DateTime.Now - startTime); 221 | var eventsResponse = new EventsResponseData 222 | { 223 | Events = eventList.ToArray(), 224 | ElapsedTime = (int)ts.TotalMilliseconds 225 | }; 226 | responseJSON = JsonConvert.SerializeObject(eventsResponse); 227 | } 228 | catch (Exception ex) 229 | { 230 | var errorResponse = new ErrorResponseData { Error = ex.ToString() }; 231 | responseJSON = JsonConvert.SerializeObject(errorResponse); 232 | } 233 | } 234 | else if (request.Url.AbsolutePath == "/matchState") 235 | { 236 | try 237 | { 238 | DateTime startTime = DateTime.Now; 239 | IAssemblyImage assemblyImage = CreateAssemblyImage(); 240 | ManagedClassInstance matchManager = (ManagedClassInstance) assemblyImage["PAPA"]["_instance"]["_matchManager"]; 241 | 242 | string matchId = matchManager.GetValue("k__BackingField"); 243 | 244 | ManagedClassInstance localPlayerInfo = (ManagedClassInstance) assemblyImage["PAPA"]["_instance"]["_matchManager"]["k__BackingField"]; 245 | 246 | float LocalMythicPercentile = localPlayerInfo.GetValue("MythicPercentile"); 247 | int LocalMythicPlacement = localPlayerInfo.GetValue("MythicPlacement"); 248 | int LocalRankingClass = localPlayerInfo.GetValue("RankingClass"); 249 | int LocalRankingTier = localPlayerInfo.GetValue("RankingTier"); 250 | 251 | ManagedClassInstance opponentInfo = (ManagedClassInstance) assemblyImage["PAPA"]["_instance"]["_matchManager"]["k__BackingField"]; 252 | 253 | float OpponentMythicPercentile = opponentInfo.GetValue("MythicPercentile"); 254 | int OpponentMythicPlacement = opponentInfo.GetValue("MythicPlacement"); 255 | int OpponentRankingClass = opponentInfo.GetValue("RankingClass"); 256 | int OpponentRankingTier = opponentInfo.GetValue("RankingTier"); 257 | 258 | TimeSpan ts = (DateTime.Now - startTime); 259 | var matchResponse = new MatchStateResponseData 260 | { 261 | MatchId = matchId, 262 | PlayerRank = new PlayerRank 263 | { 264 | MythicPercentile = LocalMythicPercentile, 265 | MythicPlacement = LocalMythicPlacement, 266 | Class = LocalRankingClass, 267 | Tier = LocalRankingTier 268 | }, 269 | OpponentRank = new PlayerRank 270 | { 271 | MythicPercentile = OpponentMythicPercentile, 272 | MythicPlacement = OpponentMythicPlacement, 273 | Class = OpponentRankingClass, 274 | Tier = OpponentRankingTier 275 | }, 276 | ElapsedTime = (int)ts.TotalMilliseconds 277 | }; 278 | responseJSON = JsonConvert.SerializeObject(matchResponse); 279 | } 280 | catch (Exception ex) 281 | { 282 | var errorResponse = new ErrorResponseData { Error = ex.ToString() }; 283 | responseJSON = JsonConvert.SerializeObject(errorResponse); 284 | } 285 | } 286 | else if (request.Url.AbsolutePath == "/allCards") 287 | { 288 | try 289 | { 290 | DateTime startTime = DateTime.Now; 291 | IAssemblyImage assemblyImage = CreateAssemblyImage(); 292 | 293 | var dbConnection = assemblyImage["WrapperController"]["k__BackingField"]["k__BackingField"]["k__BackingField"]["_baseCardDataProvider"]["_dbConnection"]; 294 | 295 | string connectionString = dbConnection["_connectionString"]; 296 | connectionString = connectionString.Replace("Data Source=Z:", "Data Source="); 297 | connectionString = connectionString.Replace("\\", "/"); 298 | 299 | var cardList = new List(); 300 | 301 | using (var connection = new SqliteConnection(connectionString)) 302 | { 303 | connection.Open(); 304 | 305 | // Get all cards with their titles 306 | var cardsCommand = connection.CreateCommand(); 307 | cardsCommand.CommandText = "SELECT c.GrpId, l.Loc as Title, c.ExpansionCode as ExpansionCode FROM Cards c JOIN Localizations_enUS l ON c.TitleId = l.LocId ORDER BY c.GrpId;"; 308 | 309 | using (var reader = cardsCommand.ExecuteReader()) 310 | { 311 | while (reader.Read()) 312 | { 313 | int grpId = reader.GetInt32(0); 314 | string title = reader.IsDBNull(1) ? "" : StringUtils.JsonEscape(reader.GetString(1)); 315 | string expansionCode = reader.IsDBNull(2) ? "" : reader.GetString(2); 316 | 317 | cardList.Add(new CardInfo { GrpId = grpId, Title = title, ExpansionCode = expansionCode }); 318 | } 319 | } 320 | } 321 | 322 | TimeSpan ts = (DateTime.Now - startTime); 323 | var allCardsResponse = new AllCardsResponseData 324 | { 325 | Cards = cardList.ToArray(), 326 | ElapsedTime = (int)ts.TotalMilliseconds 327 | }; 328 | responseJSON = JsonConvert.SerializeObject(allCardsResponse); 329 | } 330 | catch (Exception ex) 331 | { 332 | var errorResponse = new ErrorResponseData { Error = ex.ToString() }; 333 | responseJSON = JsonConvert.SerializeObject(errorResponse); 334 | } 335 | } 336 | } 337 | 338 | // Write the response info 339 | byte[] data = Encoding.UTF8.GetBytes(responseJSON); 340 | response.AddHeader("Access-Control-Allow-Origin", "*"); 341 | response.AddHeader("Access-Control-Allow-Methods", "*"); 342 | response.AddHeader("Access-Control-Allow-Headers", "*"); 343 | 344 | response.ContentType = "Application/json"; 345 | response.ContentEncoding = Encoding.UTF8; 346 | response.ContentLength64 = data.LongLength; 347 | 348 | // Write out to the response stream (asynchronously), then close it 349 | await response.OutputStream.WriteAsync(data, 0, data.Length); 350 | response.Close(); 351 | } 352 | 353 | private IAssemblyImage CreateAssemblyImage() 354 | { 355 | UnityProcessFacade unityProcess = CreateUnityProcessFacade(); 356 | return AssemblyImageFactory.Create(unityProcess, "Core"); 357 | } 358 | 359 | private UnityProcessFacade CreateUnityProcessFacade() 360 | { 361 | Process mtgaProcess = GetMTGAProcess(); 362 | if (mtgaProcess == null) 363 | { 364 | return null; 365 | } 366 | 367 | ProcessFacade processFacade; 368 | MonoLibraryOffsets monoLibraryOffsets; 369 | 370 | if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) 371 | { 372 | string memPseudoFilePath = $"/proc/{mtgaProcess.Id}/mem"; 373 | ProcessFacadeLinuxDirect processFacadeLinux = new ProcessFacadeLinuxDirect(mtgaProcess.Id, memPseudoFilePath); 374 | string gameExecutableFilePath = processFacadeLinux.GetModulePath(mtgaProcess.ProcessName); 375 | processFacade = processFacadeLinux; 376 | monoLibraryOffsets = MonoLibraryOffsets.GetOffsets(gameExecutableFilePath); 377 | } 378 | else 379 | { 380 | if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) 381 | { 382 | ProcessFacadeWindows processFacadeWindows = new ProcessFacadeWindows(mtgaProcess); 383 | monoLibraryOffsets = MonoLibraryOffsets.GetOffsets(processFacadeWindows.GetMainModuleFileName()); 384 | processFacade = processFacadeWindows; 385 | } 386 | else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) 387 | { 388 | processFacade = new ProcessFacadeMacOSDirect(mtgaProcess); 389 | monoLibraryOffsets = MonoLibraryOffsets.GetOffsets(mtgaProcess.MainModule.FileName); 390 | } 391 | else 392 | { 393 | throw new NotSupportedException("Platform not supported"); 394 | } 395 | } 396 | 397 | return new UnityProcessFacade(processFacade, monoLibraryOffsets); 398 | } 399 | 400 | private Process GetMTGAProcess() 401 | { 402 | Process[] processes = Process.GetProcesses(); 403 | if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) 404 | { 405 | foreach(Process process in processes) 406 | { 407 | if (process.ProcessName == "MTGA") 408 | { 409 | return process; 410 | } 411 | } 412 | } 413 | else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) 414 | { 415 | foreach(Process process in processes) 416 | { 417 | if (process.ProcessName == "MTGA.exe") 418 | { 419 | string maps = File.ReadAllText($"/proc/{process.Id}/maps"); 420 | if (!string.IsNullOrWhiteSpace(maps)) 421 | { 422 | return process; 423 | } 424 | } 425 | } 426 | } 427 | else 428 | { 429 | throw new NotSupportedException("Platform not supported"); 430 | } 431 | 432 | return null; 433 | } 434 | 435 | private CheckForUpdatesResponseData CheckForUpdates() 436 | { 437 | try 438 | { 439 | string latestVersionJSON = GetLatestVersionJSON(); 440 | DaemonVersion latestVersion = JsonConvert.DeserializeObject(latestVersionJSON); 441 | 442 | Console.WriteLine($"Latest version = {latestVersion.TagName}"); 443 | if(currentVersion.CompareTo(new Version(latestVersion.TagName)) < 0) 444 | { 445 | Task.Run(() => Update(latestVersion)); 446 | return new () { UpdatesAvailable = true }; 447 | } 448 | } 449 | catch (Exception ex) 450 | { 451 | Console.WriteLine($"Could not get latest version {ex}"); 452 | } 453 | 454 | return new () { UpdatesAvailable = false }; 455 | } 456 | 457 | private async Task Update(DaemonVersion latestVersion) 458 | { 459 | if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) 460 | { 461 | updating = true; 462 | Console.WriteLine("Updating..."); 463 | string targetAssetName; 464 | targetAssetName = "mtga-tracker-daemon-Linux.tar.gz"; 465 | Asset asset = latestVersion.Assets.Find(asset => asset.Name == targetAssetName); 466 | 467 | string tmpDir = "/tmp/mtga-tracker-dameon"; 468 | Directory.CreateDirectory(tmpDir); 469 | string file = Path.Combine(tmpDir, asset.Name); 470 | 471 | using (var client = new HttpClient()) 472 | { 473 | using var stream = await client.GetStreamAsync(asset.BrowserDownloadUrl); 474 | using var fileStream = new FileStream(file, FileMode.Create); 475 | await stream.CopyToAsync(fileStream); 476 | } 477 | 478 | ExtractTGZ(file, tmpDir); 479 | 480 | DirectoryInfo currentDir = new DirectoryInfo(Directory.GetCurrentDirectory()); 481 | RemoveDirectoryContentsRecursive(currentDir); 482 | 483 | Copy(Path.Combine(tmpDir, "bin"), currentDir.FullName); 484 | RemoveDirectoryContentsRecursive(new DirectoryInfo(tmpDir)); 485 | Console.WriteLine("Updated correctly"); 486 | 487 | string binary = Path.Combine(currentDir.FullName, "mtga-tracker-daemon"); 488 | ProcessStartInfo startInfo = new ProcessStartInfo() 489 | { 490 | FileName = "/bin/bash", Arguments = $"-c \"chmod +x {binary}\" && systemctl restart mtga-trackerd.service", 491 | }; 492 | Process proc = new Process() { StartInfo = startInfo, }; 493 | proc.Start(); 494 | runServer = false; 495 | listener.Stop(); 496 | Console.WriteLine("Restarting..."); 497 | } 498 | } 499 | 500 | private void RemoveDirectoryContentsRecursive(DirectoryInfo directory) { 501 | FileInfo[] oldFiles = directory.GetFiles(); 502 | foreach (FileInfo oldFile in oldFiles) 503 | { 504 | oldFile.Delete(); 505 | } 506 | 507 | foreach(DirectoryInfo childDirectory in directory.GetDirectories()) 508 | { 509 | RemoveDirectoryContentsRecursive(childDirectory); 510 | childDirectory.Delete(); 511 | } 512 | } 513 | 514 | private string GetLatestVersionJSON() 515 | { 516 | string url = "https://api.github.com/repos/frcaton/mtga-tracker-daemon/releases/latest"; 517 | 518 | using (var client = new HttpClient()) 519 | { 520 | client.DefaultRequestHeaders.Add("User-Agent", @"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.106 Safari/537.36"); 521 | return client.GetStringAsync(url).GetAwaiter().GetResult(); 522 | } 523 | } 524 | 525 | private void ExtractTGZ(string gzArchiveName, string destFolder) 526 | { 527 | Stream inStream = File.OpenRead(gzArchiveName); 528 | Stream gzipStream = new GZipInputStream(inStream); 529 | 530 | TarArchive tarArchive = TarArchive.CreateInputTarArchive(gzipStream, Encoding.UTF8); 531 | tarArchive.ExtractContents(destFolder); 532 | tarArchive.Close(); 533 | 534 | gzipStream.Close(); 535 | inStream.Close(); 536 | } 537 | 538 | private void Copy(string sourceDir, string targetDir) 539 | { 540 | Directory.CreateDirectory(targetDir); 541 | 542 | foreach(var file in Directory.GetFiles(sourceDir)) 543 | { 544 | File.Copy(file, Path.Combine(targetDir, Path.GetFileName(file))); 545 | } 546 | 547 | foreach(var directory in Directory.GetDirectories(sourceDir)) 548 | { 549 | Copy(directory, Path.Combine(targetDir, Path.GetFileName(directory))); 550 | } 551 | } 552 | } 553 | 554 | } 555 | --------------------------------------------------------------------------------