cup of acidsipacid

← Back

Writing a Phasmophobia cheat

Fri Dec 29 2023

Okay, so you’ve decided you wanna cheat in a co-op horror game, fucking weirdo. Anyway before we start we need some info about the game, like what game engine is it using, is it x64 or x86 and does it have an anticheat? Phasmophobia is made in Unity, x64 and does not have an anticheat which makes our job a lot easier.

Setting up the project

Ight, open up Visual Studio and create a new DLL C++ project. I am kinda really fucking lazy so instead of writing our own hook functions we’ll just use MS-Detours for this project. To add Detours to our project head into project properties -> Linker -> Input then add your detours.lib file to Additional Dependencies. You’ll also need to include the detours.h header file, How the fuck do I do this?. Also set the C++ Language standard and the C Language standard to latest or something similar.

Getting function addresses and structs

Phasmophobia uses something called IL2CPP which means we can’t just throw the dll in something like DnSpy and look at the code. To get past this I normally would just use Il2CppInspector but that project has been dead for quite some time and doesn’t work with the newer Unity version that Phasmophobia is using, so we’ll be using Cpp2IL instead.

To use this, all you have to do is run this command: .\Cpp2IL.exe --game-path="C:\Program Files (x86)\Steam\steamapps\common\Phasmophobia" --parallel --analyze-all or wherever you have installed your Phasmophobia. Once this is finished you can open up something like DnSpy, I’ll be using Jetbrains dotPeek for this however.

Now that we have our decompiler open we can start looking for things we want, like the ghost type. Most things will be located in the Assembly-CSharp.dll file, so open that up and look for the GhostAI class. To get the GhostAI object we can hook the GhostAI::Start function.

[Address(RVA = "0x158C940", Offset = "0x158B740", VA = "0x18158C940")]
private void Start()

We can see that the function is located at base address + RVA (0x158F920). That’s great and all but how do we get the ghost type? Below are all the fields of the GhostAI class.

[Token(Token = "0x20000E9")]
public class GhostAI : MonoBehaviour
  [Cpp2IlInjected.FieldOffset(Offset = "0x20")]
  [Token(Token = "0x40005D2")]
  private readonly ഠദവടസളഠഺദ മപജജഫലസഞട;
  [Cpp2IlInjected.FieldOffset(Offset = "0x28")]
  [Token(Token = "0x40005D3")]
  public GhostAI.വബബരരറമഞഥ ഻ഺര഻പഠനളസ;
  [Cpp2IlInjected.FieldOffset(Offset = "0x30")]
  [Token(Token = "0x40005D4")]
  public PhotonView ശനടധലപണഴവ;
  [Cpp2IlInjected.FieldOffset(Offset = "0x38")]
  [Token(Token = "0x40005D5")]
  public GhostInfo ഝറഴജഴഫഹഥജ;
  [Cpp2IlInjected.FieldOffset(Offset = "0x40")]
  [Token(Token = "0x40005D6")]
  public NavMeshAgent ലഠഴഺമഫമ഻ഠ;
  [Cpp2IlInjected.FieldOffset(Offset = "0x48")]
  [Token(Token = "0x40005D7")]
  public GhostAudio വദറതയലടമഝ;
  [Cpp2IlInjected.FieldOffset(Offset = "0x50")]
  [Token(Token = "0x40005D8")]
  public GhostInteraction ഷഥഴഡണഫഞഢഺ;
  [Cpp2IlInjected.FieldOffset(Offset = "0x58")]
  [Token(Token = "0x40005D9")]
  public GhostActivity യഞമഡശദദനണ;
  [Cpp2IlInjected.FieldOffset(Offset = "0x60")]
  [Token(Token = "0x40005DA")]
  public GhostModel നഢനനഴബഞസത;
  [Cpp2IlInjected.FieldOffset(Offset = "0x68")]
  [Token(Token = "0x40005DB")]
  private GhostModel eventModel;
  [Cpp2IlInjected.FieldOffset(Offset = "0x70")]
  [Token(Token = "0x40005DC")]
  public GhostModel[] സയഠഝഷഠറഭബ;
  [Cpp2IlInjected.FieldOffset(Offset = "0x78")]
  [Token(Token = "0x40005DD")]
  public GhostModel[] നറഫളശഴശഠണ;
  [Cpp2IlInjected.FieldOffset(Offset = "0x80")]
  [Token(Token = "0x40005DE")]
  private bool ഻ടധഢദജഞയധ;
  [Cpp2IlInjected.FieldOffset(Offset = "0x84")]
  [Token(Token = "0x40005DF")]
  public ShadowCastingMode തശപളഠളതഡവ;
  [Cpp2IlInjected.FieldOffset(Offset = "0x88")]
  [Token(Token = "0x40005E0")]
  public List<Vector3> മനഥഴഴണഢഺജ;
  [Cpp2IlInjected.FieldOffset(Offset = "0x90")]
  [Token(Token = "0x40005E1")]
  private float ഭജ഻ഢഴബശഹഫ;
  [Cpp2IlInjected.FieldOffset(Offset = "0x98")]
  [Token(Token = "0x40005E2")]
  public SanityDrainer ഻ഺഝഷബഷഩണഩ;
  [Cpp2IlInjected.FieldOffset(Offset = "0xA0")]
  [Token(Token = "0x40005E3")]
  public bool ണഺശഥശഥധതറ;
  [Cpp2IlInjected.FieldOffset(Offset = "0xA4")]
  [Token(Token = "0x40005E4")]
  public LayerMask ജഥഭടജഞലഹപ;
  [Cpp2IlInjected.FieldOffset(Offset = "0xA8")]
  [Token(Token = "0x40005E5")]
  public Transform പനതദയഢഠശസ;
  [Cpp2IlInjected.FieldOffset(Offset = "0xB0")]
  [Token(Token = "0x40005E6")]
  public Transform ജളഠദഹളഝദഝ;
  [Cpp2IlInjected.FieldOffset(Offset = "0xB8")]
  [Token(Token = "0x40005E7")]
  public Transform ബഺപടഫബധസഭ;
  [Cpp2IlInjected.FieldOffset(Offset = "0xC0")]
  [Token(Token = "0x40005E8")]
  public float പഫപഞഺദഴദഷ;
  [Cpp2IlInjected.FieldOffset(Offset = "0xC4")]
  [Token(Token = "0x40005E9")]
  public float ഢറഴഝണബനഭസ;
  [Cpp2IlInjected.FieldOffset(Offset = "0xC8")]
  [Token(Token = "0x40005EA")]
  public float ജ഻ഹ഻മഭ഻ശബ;
  [Cpp2IlInjected.FieldOffset(Offset = "0xCC")]
  [Token(Token = "0x40005EB")]
  public bool ജയമടയഩപമഞ;
  [Cpp2IlInjected.FieldOffset(Offset = "0xCD")]
  [Token(Token = "0x40005EC")]
  public bool ടലഡഹഫഢയമഩ;
  [Cpp2IlInjected.FieldOffset(Offset = "0xD0")]
  [Token(Token = "0x40005ED")]
  public Vector3 ലന഻സതഷ഻ഴഹ;
  [Cpp2IlInjected.FieldOffset(Offset = "0xE0")]
  [Token(Token = "0x40005EE")]
  public GameObject ധഺമലയളടണഥ;
  [Cpp2IlInjected.FieldOffset(Offset = "0xE8")]
  [Token(Token = "0x40005EF")]
  public bool ഥനള഻഻ഝശവഭ;
  [Cpp2IlInjected.FieldOffset(Offset = "0xE9")]
  [Token(Token = "0x40005F0")]
  public bool ഭമഫവഫസഢഞണ;
  [Cpp2IlInjected.FieldOffset(Offset = "0xEA")]
  [Token(Token = "0x40005F1")]
  public bool ണഷഺവദഠഭഹസ;
  [Cpp2IlInjected.FieldOffset(Offset = "0xEB")]
  [Token(Token = "0x40005F2")]
  public bool യഫവമളവറഺമ;
  [Cpp2IlInjected.FieldOffset(Offset = "0xF0")]
  [Token(Token = "0x40005F3")]
  public WhiteSage ഷഹഴശരഥധഩപ;
  [Cpp2IlInjected.FieldOffset(Offset = "0xF8")]
  [Token(Token = "0x40005F4")]
  private float പഴളണഞഞഷഹഥ;
  [Cpp2IlInjected.FieldOffset(Offset = "0xFC")]
  [Token(Token = "0x40005F5")]
  public bool ണഩഞബഹമഝഝന;
  [Cpp2IlInjected.FieldOffset(Offset = "0xFD")]
  [Token(Token = "0x40005F6")]
  public bool ഷധ഻തതഩലളത;
  [Cpp2IlInjected.FieldOffset(Offset = "0xFE")]
  [Token(Token = "0x40005F7")]
  public bool രഠ഻ലതബബനജ;
  [Cpp2IlInjected.FieldOffset(Offset = "0x100")]
  [Token(Token = "0x40005F8")]
  public Player ദഫഥഩഩഭഹഩമ;
  [Cpp2IlInjected.FieldOffset(Offset = "0x108")]
  [Token(Token = "0x40005F9")]
  public int റഹവധനലഷ഻ള;
  [Cpp2IlInjected.FieldOffset(Offset = "0x10C")]
  [Token(Token = "0x40005FA")]
  public Vector3 ഭണഺവഢഞരഹ഻;
  [Cpp2IlInjected.FieldOffset(Offset = "0x118")]
  [Token(Token = "0x40005FB")]
  private readonly float[] ജഡജസഠമററല;
  [Cpp2IlInjected.FieldOffset(Offset = "0x120")]
  [Token(Token = "0x40005FC")]
  private readonly float[] നസഩണഥവവരഭ;
  [Cpp2IlInjected.FieldOffset(Offset = "0x128")]
  [Token(Token = "0x40005FD")]
  private readonly float[] ഝയഩബതഥഥശയ;
  [Cpp2IlInjected.FieldOffset(Offset = "0x130")]
  [Token(Token = "0x40005FE")]
  private int ഡലടഫഡണഭഢഷ;
  [Cpp2IlInjected.FieldOffset(Offset = "0x134")]
  [Token(Token = "0x40005FF")]
  private int ധശഷണനരളളഹ;
  [Cpp2IlInjected.FieldOffset(Offset = "0x138")]
  [Token(Token = "0x4000600")]
  private int ഩഞബഴഝഥഺദധ;
  [Cpp2IlInjected.FieldOffset(Offset = "0x13C")]
  [Token(Token = "0x4000601")]
  private int മഹധശതദഫഫഷ;
  [Cpp2IlInjected.FieldOffset(Offset = "0x140")]
  [Token(Token = "0x4000602")]
  private int ഩധഝഭളഠയശര;
  [Cpp2IlInjected.FieldOffset(Offset = "0x148")]
  [Token(Token = "0x4000603")]
  private readonly float[] ണഭബധബശവ഻ണ;
  [Cpp2IlInjected.FieldOffset(Offset = "0x150")]
  [Token(Token = "0x4000604")]
  private readonly float[] ഹഡ഻റനഫഫഝന;
  [Cpp2IlInjected.FieldOffset(Offset = "0x158")]
  [Token(Token = "0x4000605")]
  private readonly float[] വബശഠജണളഠഺ;
  [Cpp2IlInjected.FieldOffset(Offset = "0x160")]
  [Token(Token = "0x4000606")]
  private float ഩളഢരറരധ഻ഴ;
  [Cpp2IlInjected.FieldOffset(Offset = "0x168")]
  [Token(Token = "0x4000607")]
  private readonly int[] ഭഢഝഭതഫദവ഻;
  [Cpp2IlInjected.FieldOffset(Offset = "0x170")]
  [Token(Token = "0x4000608")]
  private readonly int[] ശയഹഡറഡഥടല;
  [Cpp2IlInjected.FieldOffset(Offset = "0x178")]
  [Token(Token = "0x4000609")]
  private readonly int[] ധലഢപബഡമഭല;
  [Cpp2IlInjected.FieldOffset(Offset = "0x180")]
  [Token(Token = "0x400060A")]
  private readonly int[] ഻ണഫസഫയഠസന;

Ghost type is probably an enum but where is it? You can find it in the GhostTraits class, to get there, go the GhostInfo class then go the first type that is being used in the GhostInfo class, in this case it is പദഴലടഫഺഷവ.

[Token(Token = "0x20000F7")]
public class GhostInfo : MonoBehaviourPun
  [FieldOffset(Offset = "0x28")]
  [Token(Token = "0x4000652")]
  public പദഴലടഫഺഷവ ളവയഥസരഹഥട;
  [FieldOffset(Offset = "0x68")]
  [Token(Token = "0x4000653")]
  private GhostAI ghost;
  [FieldOffset(Offset = "0x70")]
  [Token(Token = "0x4000654")]
  public LevelRoom ഹഷഹധതജഺലള;
  [FieldOffset(Offset = "0x78")]
  [Token(Token = "0x4000655")]
  public float ഫഥമതഹഷഫഩബ;
  [FieldOffset(Offset = "0x7C")]
  [Token(Token = "0x4000656")]
  private bool ണടറളശഥസറപ;

Which will bring us to this struct, the first field (ബണജഷസഷണഞഴ) is the ghost type and the second one (ഹഭഴഞഴന\u0D3Bഥഝ) is the mimic type.

[Token(Token = "0x200015E")]
public struct പദഴലടഫഺഷവ
  [FieldOffset(Offset = "0x0")]
  [Token(Token = "0x400086D")]
  public പദഴലടഫഺഷവ.ഝഡശഭഡഡഷഴജ ബണജഷസഷണഞഴ;
  [FieldOffset(Offset = "0x4")]
  [Token(Token = "0x400086E")]
  public പദഴലടഫഺഷവ.ഝഡശഭഡഡഷഴജ ഹഭഴഞഴന഻ഥഝ;
  [FieldOffset(Offset = "0x8")]
  [Token(Token = "0x400086F")]
  public List<ദസഫനഠശഢപവ> ഠഥളഡഠലഹമത;
  [FieldOffset(Offset = "0x10")]
  [Token(Token = "0x4000870")]
  public List<ദസഫനഠശഢപവ> ഭടഞ഻റഝമനത;
  [FieldOffset(Offset = "0x18")]
  [Token(Token = "0x4000871")]
  public int ഻ദഞന഻ഫഺടഡ;
  [FieldOffset(Offset = "0x1C")]
  [Token(Token = "0x4000872")]
  public bool ഥഭദടബശനദവ;
  [FieldOffset(Offset = "0x20")]
  [Token(Token = "0x4000873")]
  public string തണവബഝഝഹവഹ;
  [FieldOffset(Offset = "0x28")]
  [Token(Token = "0x4000874")]
  public int തഺറഡദഠഹരജ;
  [FieldOffset(Offset = "0x2C")]
  [Token(Token = "0x4000875")]
  public int സയഡലറണഷദന;
  [FieldOffset(Offset = "0x30")]
  [Token(Token = "0x4000876")]
  public bool ദജഷനജഭധജദ;
  [FieldOffset(Offset = "0x34")]
  [Token(Token = "0x4000877")]
  public int ബഭഞപരയധഠണ;
  [FieldOffset(Offset = "0x38")]
  [Token(Token = "0x4000878")]
  public int യലളനഹഝയതഠ;
  [FieldOffset(Offset = "0x3C")]
  [Token(Token = "0x4000879")]
  public bool ല഻സഺഢവസളഫ;

  [Token(Token = "0x200015F")]
  public enum ഝഡശഭഡഡഷഴജ
    [Token(Token = "0x400087B")] Spirit,
    [Token(Token = "0x400087C")] Wraith,
    [Token(Token = "0x400087D")] Phantom,
    [Token(Token = "0x400087E")] Poltergeist,
    [Token(Token = "0x400087F")] Banshee,
    [Token(Token = "0x4000880")] Jinn,
    [Token(Token = "0x4000881")] Mare,
    [Token(Token = "0x4000882")] Revenant,
    [Token(Token = "0x4000883")] Shade,
    [Token(Token = "0x4000884")] Demon,
    [Token(Token = "0x4000885")] Yurei,
    [Token(Token = "0x4000886")] Oni,
    [Token(Token = "0x4000887")] Yokai,
    [Token(Token = "0x4000888")] Hantu,
    [Token(Token = "0x4000889")] Goryo,
    [Token(Token = "0x400088A")] Myling,
    [Token(Token = "0x400088B")] Onryo,
    [Token(Token = "0x400088C")] TheTwins,
    [Token(Token = "0x400088D")] Raiju,
    [Token(Token = "0x400088E")] Obake,
    [Token(Token = "0x400088F")] Mimic,
    [Token(Token = "0x4000890")] Moroi,
    [Token(Token = "0x4000891")] Deogen,
    [Token(Token = "0x4000892")] Thaye,

Creating our SDK

Back in Visual Studio we are, let’s start by creating a simple SDK that we can use. I’ll create a new header file called sdk.h and add a base address and a macro to define functions. I’ll also create another header file GhostAI.h and add the GhostAI structs to it and define the GhostAI::Start function. Because GhostAI derives from MonoBehaviour and GhostInfo from MonoBehaviourPun we’ll have to add those fields to the GhostAIFields struct too, I’ll give you the MonoBehaviour and the MonoBehaviourPun structs. We won’t need to add every field from the actual GhostAI class, just the ones we need; so up until the GhostInfo field.

// sdk.h
#pragma once

using NAME = TYPE; 
inline NAME NAME##_ptr = reinterpret_cast<NAME>(BASE_ADDRESS + ADDRESS);

namespace SDK
  const auto BASE_ADDRESS = reinterpret_cast<uintptr_t>(GetModuleHandleW(L"GameAssembly.dll"));

// MonoBehaviour.h
namespace SDK
  struct __declspec(align(8)) Object1Fields
    void* m_CachedPtr;
    void* m_CancellationTokenSource;

  struct Component1Fields
    Object1Fields _;

  struct BehaviourFields
    Component1Fields _;

  struct MonoBehaviourFields
    BehaviourFields _;

  struct MonoBehaviour
    void* Clazz; // MonoBehaviourClass
    void* Monitor; // MonitorData
    MonoBehaviourFields Fields;

  struct MonoBehaviourPunFields
    MonoBehaviourFields _;
    void* pvCache;

  struct MonoBehaviourPunCallbacksFields
    MonoBehaviourPunFields _;

// GhostAI.h
#pragma once
#include "sdk.h"

namespace SDK
  struct GhostAIFields
    MonoBehaviourFields _;
    void* Field0;
    int32_t Field1;
    void* Field2;
    GhostInfo* GhostInfo; // Only field we care about

  struct GhostAI
    void* Clazz; // GhostAI class
    void* Monitor; // Monitor Data
    GhostAIFields Fields;

  DECLARE_FUNCTION_POINTER(GhostAI_Start, void(*)(GhostAI* ghostAI, void* methodInfo), 0x158C940);

Okay great, now we’ll need to create the GhostInfo struct and the GhostTrait struct. Don’t forget to include them in the sdk.h. We will need every field from the GhostTrait struct because the GhostType field will only be valid after Name isn’t a nullptr anymore.

// GhostInfo.h
#pragma once
#include "sdk.h"

namespace SDK
  struct GhostInfoFields
    MonoBehaviourPunFields _;
    GhostTraits GhostTraits; // very important 1!!!1
    // don't care about other fields

  struct GhostInfo
    void* Clazz; // GhostInfo class
    void* Monitor; // Monitor Data
    GhostInfoFields Fields;

// GhostTraits.h
#pragma once
#include "sdk.h"

namespace SDK
  enum class GhostType: int32_t

  struct GhostTraits
    GhostType GhostType_;
    GhostType MimicType;
    // don't care about the other ones
    void* Field2;
    void* Field3;
    int32_t Field4;
    bool Field5;
    void* Name;
    int32_t Field7;
    int Field8;
    bool Field9;
    int32_t Field10;
    int32_t Field11;
    bool Field12;

Hooking the GhostAI::Start function

Back in our dllmain.cpp we can create our “HackThread”, define our GhostAI_Start function and hook it using Detours.

// dllmain.cpp
HMODULE hHackModule = nullptr;
HANDLE hHackThread = nullptr;

void hkGhostAI_Start(SDK::GhostAI* _ghostAI, void* methodInfo)
  // Calling original function
  SDK::GhostAI_Start_ptr(_ghostAI, methodInfo);

  // Detouring GhostAI.Start
  DetourAttach(&reinterpret_cast<PVOID&>(SDK::GhostAI_Start_ptr), hkGhostAI_Start);

  // No exiting the cheat yet
  while (true)
    if (GetAsyncKeyState(VK_END) & 1)


  // Un-detouring GhostAI.Start
  DetourDetach(&reinterpret_cast<PVOID&>(SDK::GhostAI_Start_ptr), hkGhostAI_Start);

  FreeLibraryAndExitThread(hHackModule, 0);

BOOL APIENTRY DllMain(HMODULE hModule, DWORD ulReasonForCall, LPVOID lpReserved)
  if (ulReasonForCall == DLL_PROCESS_ATTACH)
    hHackModule = hModule;
    hHackThread = CreateThread(nullptr, 0, reinterpret_cast<LPTHREAD_START_ROUTINE>(HackThread), hModule, 0, nullptr);

  return TRUE;

Displaying the ghost type

Woah almost there, now to display the ghost type I’ll just add a simple console using AllocConsole. We’ll also need to save the GhostAI pointer to a global variable so we can use it.

// dllmain.cpp
SDK::GhostAI* ghostAI = nullptr;

void hkGhostAI_Start(SDK::GhostAI* _ghostAI, void* methodInfo)
  // Saving ghostAI pointer
  ghostAI = _ghostAI;

  // Calling original function
  SDK::GhostAI_Start_ptr(ghostAI, methodInfo);

  // Beep boop I'm a console
  FILE* f;
  freopen_s(&f, "CONOUT$", "w", stdout);

  // other code

  // when exiting after removing hook
  // Bye bye console

Cool, now that we can print stuff to a console, let’s print the ghost type. To do this we’ll need to convert the enum to a string, I’ll just use a switch statement for this because I’m stupid. We will print via the HackThread while loop ‘cause ghostInfo can be a nullptr.

// GhostTraits.h
inline std::string GhostTypeToString(GhostType ghostType)
  switch (ghostType)
  case GhostType::Spirit:
    return "Spirit";
  case GhostType::Wraith:
    return "Wraith";
  case GhostType::Phantom:
    return "Phantom";
  case GhostType::Poltergeist:
    return "Poltergeist";
  case GhostType::Banshee:
    return "Banshee";
  case GhostType::Jinn:
    return "Jinn";
  case GhostType::Mare:
    return "Mare";
  case GhostType::Revenant:
    return "Revenant";
  case GhostType::Shade:
    return "Shade";
  case GhostType::Demon:
    return "Demon";
  case GhostType::Yurei:
    return "Yurei";
  case GhostType::Oni:
    return "Oni";
  case GhostType::Yokai:
    return "Yokai";
  case GhostType::Hantu:
    return "Hantu";
  case GhostType::Goryo:
    return "Goryo";
  case GhostType::Myling:
    return "Myling";
  case GhostType::Onryo:
    return "Onryo";
  case GhostType::TheTwins:
    return "The Twins";
  case GhostType::Raiju:
    return "Raiju";
  case GhostType::Obake:
    return "Obake";
  case GhostType::Mimic:
    return "Mimic";
  case GhostType::Moroi:
    return "Moroi";
  case GhostType::Deogen:
    return "Deogen";
  case GhostType::Thaye:
    return "Thaye";
    return "Unknown";

// dllmain.cpp
bool shouldPrint = false;

void hkGhostAI_Start(SDK::GhostAI* _ghostAI, void* methodInfo)
  // Saving ghostAI pointer
  ghostAI = _ghostAI;
  shouldPrint = true;

  // Calling original function
  SDK::GhostAI_Start_ptr(ghostAI, methodInfo);

    // code

    while (true)
      /// oooooo spooky ghost
      if (shouldPrint && ghostAI)
        if (const auto ghostInfo = ghostAI->Fields.GhostInfo)
          // if name is a nullptr then the ghost type isn't valid yet
          if (ghostInfo->Fields.GhostTraits.Name)
            const auto ghostType = ghostInfo->Fields.GhostTraits.GhostType_;
            std::cout << "Ghost type: " << GhostTypeToString(ghostType) << std::endl;
            shouldPrint = false;

      if (GetAsyncKeyState(VK_END) & 1)


    // more code


During the development of Asthmaphobia I found out that you can create an anti-kick hack by just hooking ServerManager_KickPlayerNetworked and then not calling the original function :) Have fun.


That’s it, the basics for creating your own shitty Phasmophobia cheat. Source code for this project can be found on GitHub. Yes I know the code is bad, it isn’t meant to be good code.