2.0.3 #123
@@ -127,79 +127,23 @@ public class PlayerDataFactory
|
||||
{
|
||||
nint basePtr = playerPointer;
|
||||
|
||||
if (!LooksLikeUserPtr(basePtr))
|
||||
if (!PtrGuard.LooksLikePtr(basePtr))
|
||||
return true;
|
||||
|
||||
nint drawObjAddr = basePtr + _drawObjectOffset;
|
||||
|
||||
if (!TryReadIntPtr(drawObjAddr, out var drawObj))
|
||||
if (!PtrGuard.IsReadable(drawObjAddr, (nuint)IntPtr.Size))
|
||||
return true;
|
||||
|
||||
if (!PtrGuard.TryReadIntPtr(drawObjAddr, out var drawObj))
|
||||
return true;
|
||||
|
||||
if (drawObj != 0 && !PtrGuard.LooksLikePtr(drawObj))
|
||||
return true;
|
||||
|
||||
return drawObj == 0;
|
||||
}).ConfigureAwait(false);
|
||||
|
||||
private static bool LooksLikeUserPtr(nint p)
|
||||
{
|
||||
if (p == 0) return false;
|
||||
|
||||
ulong u = (ulong)p;
|
||||
|
||||
if (u < 0x0000_0001_0000UL) return false;
|
||||
if (u > 0x0000_7FFF_FFFF_FFFFUL) return false;
|
||||
if ((u & 0x7UL) != 0) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryReadIntPtr(nint addr, out nint value)
|
||||
{
|
||||
value = 0;
|
||||
|
||||
if (!VirtualReadable(addr))
|
||||
return false;
|
||||
|
||||
try
|
||||
{
|
||||
value = Marshal.ReadIntPtr(addr);
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool VirtualReadable(nint addr)
|
||||
{
|
||||
if (VirtualQuery(addr, out var mbi, (nuint)Marshal.SizeOf<MEMORY_BASIC_INFORMATION>()) == 0)
|
||||
return false;
|
||||
|
||||
const uint MEM_COMMIT = 0x1000;
|
||||
const uint PAGE_NOACCESS = 0x01;
|
||||
const uint PAGE_GUARD = 0x100;
|
||||
|
||||
if (mbi.State != MEM_COMMIT) return false;
|
||||
if ((mbi.Protect & PAGE_GUARD) != 0) return false;
|
||||
if (mbi.Protect == PAGE_NOACCESS) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
private static extern nuint VirtualQuery(nint lpAddress, out MEMORY_BASIC_INFORMATION lpBuffer, nuint dwLength);
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct MEMORY_BASIC_INFORMATION
|
||||
{
|
||||
public nint BaseAddress;
|
||||
public nint AllocationBase;
|
||||
public uint AllocationProtect;
|
||||
public nuint RegionSize;
|
||||
public uint State;
|
||||
public uint Protect;
|
||||
public uint Type;
|
||||
}
|
||||
|
||||
private static bool IsCacheFresh(CacheEntry entry)
|
||||
=> (DateTime.UtcNow - entry.CreatedUtc) <= _characterCacheTtl;
|
||||
|
||||
|
||||
@@ -2,11 +2,12 @@
|
||||
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
|
||||
using LightlessSync.Services;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using LightlessSync.Utils;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Runtime.CompilerServices;
|
||||
using static FFXIVClientStructs.FFXIV.Client.Game.Character.DrawDataContainer;
|
||||
using VisibilityFlags = FFXIVClientStructs.FFXIV.Client.Game.Object.VisibilityFlags;
|
||||
using ObjectKind = LightlessSync.API.Data.Enum.ObjectKind;
|
||||
using VisibilityFlags = FFXIVClientStructs.FFXIV.Client.Game.Object.VisibilityFlags;
|
||||
|
||||
namespace LightlessSync.PlayerData.Handlers;
|
||||
|
||||
@@ -177,18 +178,47 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
||||
var prevDrawObj = DrawObjectAddress;
|
||||
string? nameString = null;
|
||||
|
||||
Address = _getAddress();
|
||||
// Resolve address and validate BEFORE first deref
|
||||
var nextAddr = _getAddress();
|
||||
|
||||
// Optional: catch the root cause quickly
|
||||
// if nextAddr is 32-bit-ish, you're being fed an id/sentinel
|
||||
if (nextAddr != IntPtr.Zero && !PtrGuard.LooksLikePtr(nextAddr))
|
||||
{
|
||||
Logger.LogWarning("[{this}] _getAddress returned non-pointer: 0x{addr:X}", this, (ulong)nextAddr);
|
||||
nextAddr = IntPtr.Zero;
|
||||
}
|
||||
|
||||
// Must be readable at least for a GameObject header before touching it
|
||||
if (nextAddr != IntPtr.Zero &&
|
||||
!PtrGuard.IsReadable(nextAddr, (nuint)sizeof(FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject)))
|
||||
{
|
||||
Logger.LogWarning("[{this}] Address not readable: 0x{addr:X}", this, (ulong)nextAddr);
|
||||
nextAddr = IntPtr.Zero;
|
||||
}
|
||||
|
||||
Address = nextAddr;
|
||||
|
||||
if (Address != IntPtr.Zero)
|
||||
{
|
||||
var gameObject = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)Address;
|
||||
DrawObjectAddress = (IntPtr)gameObject->DrawObject;
|
||||
|
||||
var draw = (nint)gameObject->DrawObject;
|
||||
|
||||
if (!PtrGuard.LooksLikePtr(draw) || !PtrGuard.IsReadable(draw, (nuint)sizeof(DrawObject)))
|
||||
draw = 0;
|
||||
|
||||
DrawObjectAddress = draw;
|
||||
EntityId = gameObject->EntityId;
|
||||
|
||||
var chara = (Character*)Address;
|
||||
nameString = chara->GameObject.NameString;
|
||||
if (!string.IsNullOrEmpty(nameString) && !string.Equals(nameString, Name, StringComparison.Ordinal))
|
||||
Name = nameString;
|
||||
if (PtrGuard.IsReadable(Address, (nuint)sizeof(Character)))
|
||||
{
|
||||
var chara = (Character*)Address;
|
||||
nameString = chara->GameObject.NameString;
|
||||
|
||||
if (!string.IsNullOrEmpty(nameString) && !string.Equals(nameString, Name, StringComparison.Ordinal))
|
||||
Name = nameString;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -196,22 +226,27 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
||||
EntityId = uint.MaxValue;
|
||||
}
|
||||
|
||||
CurrentDrawCondition = IsBeingDrawnUnsafe();
|
||||
CurrentDrawCondition = (Address != IntPtr.Zero && DrawObjectAddress != IntPtr.Zero)
|
||||
? IsBeingDrawnUnsafe()
|
||||
: DrawCondition.DrawObjectZero;
|
||||
|
||||
if (_haltProcessing || !allowPublish) return;
|
||||
|
||||
bool drawObjDiff = DrawObjectAddress != prevDrawObj;
|
||||
bool addrDiff = Address != prevAddr;
|
||||
|
||||
if (Address != IntPtr.Zero && DrawObjectAddress != IntPtr.Zero)
|
||||
if (Address != IntPtr.Zero && DrawObjectAddress != IntPtr.Zero
|
||||
&& PtrGuard.IsReadable(Address, (nuint)sizeof(Character))
|
||||
&& PtrGuard.IsReadable(DrawObjectAddress, (nuint)sizeof(DrawObject)))
|
||||
{
|
||||
var chara = (Character*)Address;
|
||||
var drawObj = (DrawObject*)DrawObjectAddress;
|
||||
|
||||
var objType = drawObj->Object.GetObjectType();
|
||||
var isHuman = objType == ObjectType.CharacterBase
|
||||
&& ((CharacterBase*)drawObj)->GetModelType() == CharacterBase.ModelType.Human;
|
||||
|
||||
nameString ??= ((Character*)Address)->GameObject.NameString;
|
||||
nameString ??= chara->GameObject.NameString;
|
||||
var nameChange = !string.Equals(nameString, Name, StringComparison.Ordinal);
|
||||
if (nameChange) Name = nameString;
|
||||
|
||||
@@ -219,32 +254,36 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
||||
|
||||
if (isHuman)
|
||||
{
|
||||
var classJob = chara->CharacterData.ClassJob;
|
||||
if (classJob != _classJob)
|
||||
if (PtrGuard.IsReadable(DrawObjectAddress, (nuint)sizeof(Human)))
|
||||
{
|
||||
Logger.LogTrace("[{this}] classjob changed from {old} to {new}", this, _classJob, classJob);
|
||||
_classJob = classJob;
|
||||
Mediator.Publish(new ClassJobChangedMessage(this));
|
||||
var classJob = chara->CharacterData.ClassJob;
|
||||
if (classJob != _classJob)
|
||||
{
|
||||
Logger.LogTrace("[{this}] classjob changed from {old} to {new}", this, _classJob, classJob);
|
||||
_classJob = classJob;
|
||||
Mediator.Publish(new ClassJobChangedMessage(this));
|
||||
}
|
||||
|
||||
equipDiff = CompareAndUpdateEquipByteData((byte*)&((Human*)drawObj)->Head);
|
||||
|
||||
ref var mh = ref chara->DrawData.Weapon(WeaponSlot.MainHand);
|
||||
ref var oh = ref chara->DrawData.Weapon(WeaponSlot.OffHand);
|
||||
|
||||
equipDiff |= CompareAndUpdateMainHand((Weapon*)mh.DrawObject);
|
||||
equipDiff |= CompareAndUpdateOffHand((Weapon*)oh.DrawObject);
|
||||
}
|
||||
else
|
||||
{
|
||||
isHuman = false;
|
||||
}
|
||||
|
||||
equipDiff = CompareAndUpdateEquipByteData((byte*)&((Human*)drawObj)->Head);
|
||||
|
||||
ref var mh = ref chara->DrawData.Weapon(WeaponSlot.MainHand);
|
||||
ref var oh = ref chara->DrawData.Weapon(WeaponSlot.OffHand);
|
||||
equipDiff |= CompareAndUpdateMainHand((Weapon*)mh.DrawObject);
|
||||
equipDiff |= CompareAndUpdateOffHand((Weapon*)oh.DrawObject);
|
||||
|
||||
if (equipDiff)
|
||||
Logger.LogTrace("Checking [{this}] equip data as human from draw obj, result: {diff}", this, equipDiff);
|
||||
}
|
||||
else
|
||||
|
||||
if (!isHuman)
|
||||
{
|
||||
equipDiff = CompareAndUpdateEquipByteData((byte*)Unsafe.AsPointer(ref chara->DrawData.EquipmentModelIds[0]));
|
||||
if (equipDiff)
|
||||
Logger.LogTrace("Checking [{this}] equip data from game obj, result: {diff}", this, equipDiff);
|
||||
}
|
||||
|
||||
if (equipDiff && !_isOwnedObject) // send the message out immediately and cancel out, no reason to continue if not self
|
||||
if (equipDiff && !_isOwnedObject)
|
||||
{
|
||||
Logger.LogTrace("[{this}] Changed", this);
|
||||
return;
|
||||
@@ -252,11 +291,13 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
||||
|
||||
bool customizeDiff = false;
|
||||
|
||||
if (isHuman)
|
||||
if (isHuman && PtrGuard.IsReadable(DrawObjectAddress, (nuint)sizeof(Human)))
|
||||
{
|
||||
var gender = ((Human*)drawObj)->Customize.Sex;
|
||||
var raceId = ((Human*)drawObj)->Customize.Race;
|
||||
var tribeId = ((Human*)drawObj)->Customize.Tribe;
|
||||
var human = (Human*)drawObj;
|
||||
|
||||
var gender = human->Customize.Sex;
|
||||
var raceId = human->Customize.Race;
|
||||
var tribeId = human->Customize.Tribe;
|
||||
|
||||
if (_isOwnedObject && ObjectKind == ObjectKind.Player
|
||||
&& (gender != Gender || raceId != RaceId || tribeId != TribeId))
|
||||
@@ -267,15 +308,11 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
||||
TribeId = tribeId;
|
||||
}
|
||||
|
||||
customizeDiff = CompareAndUpdateCustomizeData(((Human*)drawObj)->Customize.Data);
|
||||
if (customizeDiff)
|
||||
Logger.LogTrace("Checking [{this}] customize data as human from draw obj, result: {diff}", this, customizeDiff);
|
||||
customizeDiff = CompareAndUpdateCustomizeData(human->Customize.Data);
|
||||
}
|
||||
else
|
||||
{
|
||||
customizeDiff = CompareAndUpdateCustomizeData(chara->DrawData.CustomizeData.Data);
|
||||
if (customizeDiff)
|
||||
Logger.LogTrace("Checking [{this}] customize data from game obj, result: {diff}", this, equipDiff);
|
||||
}
|
||||
|
||||
if ((addrDiff || drawObjDiff || equipDiff || customizeDiff || nameChange) && _isOwnedObject)
|
||||
@@ -289,12 +326,11 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
||||
CurrentDrawCondition = DrawCondition.DrawObjectZero;
|
||||
Logger.LogTrace("[{this}] Changed", this);
|
||||
if (_isOwnedObject && ObjectKind != ObjectKind.Player)
|
||||
{
|
||||
Mediator.Publish(new ClearCacheForObjectMessage(this));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private unsafe bool CompareAndUpdateCustomizeData(Span<byte> customizeData)
|
||||
{
|
||||
bool hasChanges = false;
|
||||
@@ -330,7 +366,10 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
||||
|
||||
private unsafe bool CompareAndUpdateMainHand(Weapon* weapon)
|
||||
{
|
||||
if ((nint)weapon == nint.Zero) return false;
|
||||
var p = (nint)weapon;
|
||||
if (!PtrGuard.LooksLikePtr(p) || !PtrGuard.IsReadable(p, (nuint)sizeof(Weapon)))
|
||||
return false;
|
||||
|
||||
bool hasChanges = false;
|
||||
hasChanges |= weapon->ModelSetId != MainHandData[0];
|
||||
MainHandData[0] = weapon->ModelSetId;
|
||||
@@ -343,7 +382,10 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
||||
|
||||
private unsafe bool CompareAndUpdateOffHand(Weapon* weapon)
|
||||
{
|
||||
if ((nint)weapon == nint.Zero) return false;
|
||||
var p = (nint)weapon;
|
||||
if (!PtrGuard.LooksLikePtr(p) || !PtrGuard.IsReadable(p, (nuint)sizeof(Weapon)))
|
||||
return false;
|
||||
|
||||
bool hasChanges = false;
|
||||
hasChanges |= weapon->ModelSetId != OffHandData[0];
|
||||
OffHandData[0] = weapon->ModelSetId;
|
||||
|
||||
55
LightlessSync/Utils/PtrGuard.cs
Normal file
55
LightlessSync/Utils/PtrGuard.cs
Normal file
@@ -0,0 +1,55 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using static LightlessSync.Utils.PtrGuardMemory;
|
||||
|
||||
namespace LightlessSync.Utils
|
||||
{
|
||||
public static partial class PtrGuard
|
||||
{
|
||||
private const ulong _minLikelyPtr = 0x0000_0001_0000_0000UL;
|
||||
private const ulong _maxUserPtr = 0x0000_7FFF_FFFF_FFFFUL;
|
||||
private const ulong _aligmentPtr = 0x7UL;
|
||||
|
||||
public static bool LooksLikePtr(nint p)
|
||||
{
|
||||
if (p == 0) return false;
|
||||
var u = (ulong)p;
|
||||
if (u < _minLikelyPtr) return false;
|
||||
if (u > _maxUserPtr) return false;
|
||||
if ((u & _aligmentPtr) != 0) return false;
|
||||
return true;
|
||||
}
|
||||
public static bool TryReadIntPtr(nint addr, out nint value)
|
||||
{
|
||||
value = 0;
|
||||
|
||||
if (!LooksLikePtr(addr))
|
||||
return false;
|
||||
|
||||
return ReadProcessMemory(GetCurrentProcess(), addr, out value, (nuint)IntPtr.Size, out nuint bytesRead)
|
||||
&& bytesRead == (nuint)IntPtr.Size;
|
||||
}
|
||||
|
||||
public static bool IsReadable(nint addr, nuint size)
|
||||
{
|
||||
if (addr == 0 || size == 0) return false;
|
||||
|
||||
if (VirtualQuery(addr, out var mbi, (nuint)Marshal.SizeOf<MEMORY_BASIC_INFORMATION>()) == 0)
|
||||
return false;
|
||||
|
||||
const uint Commit = 0x1000;
|
||||
const uint NoAccess = 0x01;
|
||||
const uint PageGuard = 0x100;
|
||||
|
||||
if (mbi.State != Commit) return false;
|
||||
if ((mbi.Protect & PageGuard) != 0) return false;
|
||||
if (mbi.Protect == NoAccess) return false;
|
||||
|
||||
ulong start = (ulong)addr;
|
||||
ulong end = start + size - 1;
|
||||
ulong r0 = (ulong)mbi.BaseAddress;
|
||||
ulong r1 = r0 + mbi.RegionSize - 1;
|
||||
|
||||
return start >= r0 && end <= r1;
|
||||
}
|
||||
}
|
||||
}
|
||||
36
LightlessSync/Utils/PtrGuardMemory.cs
Normal file
36
LightlessSync/Utils/PtrGuardMemory.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace LightlessSync.Utils
|
||||
{
|
||||
internal static class PtrGuardMemory
|
||||
{
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
internal struct MEMORY_BASIC_INFORMATION
|
||||
{
|
||||
public nint BaseAddress;
|
||||
public nint AllocationBase;
|
||||
public uint AllocationProtect;
|
||||
public nuint RegionSize;
|
||||
public uint State;
|
||||
public uint Protect;
|
||||
public uint Type;
|
||||
}
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
internal static extern nuint VirtualQuery(
|
||||
nint lpAddress,
|
||||
out MEMORY_BASIC_INFORMATION lpBuffer,
|
||||
nuint dwLength);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
internal static extern bool ReadProcessMemory(
|
||||
nint hProcess,
|
||||
nint lpBaseAddress,
|
||||
out nint lpBuffer,
|
||||
nuint nSize,
|
||||
out nuint lpNumberOfBytesRead);
|
||||
|
||||
[DllImport("kernel32.dll")]
|
||||
internal static extern nint GetCurrentProcess();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user