Compare commits

...

13 Commits

Author SHA1 Message Date
vietdungdev
899bed1b1c Merge branch 'feature/secure-v3b' into develop 2026-06-09 10:06:01 +07:00
vietdungdev
3219e9c851 update terror offset 2026-06-09 10:05:42 +07:00
vietdungdev
a52ef1fe63 Revert "improve terror zone"
This reverts commit e7e82817c4.
2026-06-08 21:25:24 +07:00
vietdungdev
e7e82817c4 improve terror zone 2026-06-08 21:12:58 +07:00
vietdungdev
2a899bdfb2 fix check type charm and torch 2026-06-07 19:32:47 +07:00
vietdungdev
9408f1f980 update red portal 2026-06-07 10:58:13 +07:00
vietdungdev
bce4d07041 update allres 2026-05-30 20:39:47 +07:00
vietdungdev
439bf27cb2 update belt data 2026-05-30 19:49:47 +07:00
vietdungdev
c2be2c1816 improve item.go 3 2026-05-25 17:31:42 +07:00
vietdungdev
63e001982c improve item.go 2 2026-05-25 12:14:28 +07:00
vietdungdev
a0a2a67e60 secure 2026-05-23 19:41:30 +07:00
vietdungdev
deb8c37b55 remove unused code 2026-04-26 13:34:52 +07:00
vietdungdev
8173a628b2 secure v2 2026-03-21 12:17:47 +07:00
11 changed files with 388 additions and 128 deletions

View File

@@ -30,14 +30,16 @@ func (b Belt) GetFirstPotion(potionType PotionType) (Position, bool) {
func (b Belt) Rows() int { func (b Belt) Rows() int {
switch b.Name { switch b.Name {
case "":
return 1
case "Sash", "LightBelt": case "Sash", "LightBelt":
return 2 return 2
case "Belt", "HeavyBelt": case "Belt", "HeavyBelt":
return 3 return 3
default: case "PlatedBelt",
"DemonhideSash", "SharkskinBelt", "MeshBelt", "BattleBelt", "WarBelt",
"SpiderwebSash", "VampirefangBelt", "MithrilCoil", "TrollBelt", "ColossusGirdle":
return 4 return 4
default:
return 1
} }
} }

View File

@@ -13,3 +13,15 @@ type Type struct {
func (t Type) IsType(typeName string) bool { func (t Type) IsType(typeName string) bool {
return t.Code == typeName return t.Code == typeName
} }
func (itmType Type) IsCharmOrTorch() bool {
if itmType.IsType(TypeCharm) ||
itmType.IsType(TypeSmallCharm) ||
itmType.IsType(TypeMediumCharm) ||
itmType.IsType(TypeLargeCharm) ||
itmType.IsType(TypeCraftedSunderCharm) ||
itmType.IsType(TypeTorch) {
return true
}
return false
}

View File

@@ -99,7 +99,8 @@ func (o Object) IsRedPortal() bool {
o.Name == object.BaalsPortal || o.Name == object.BaalsPortal ||
o.Name == object.ArcaneSanctuaryPortal || o.Name == object.ArcaneSanctuaryPortal ||
o.Name == object.LastLastPortal || o.Name == object.LastLastPortal ||
o.Name == object.HellGate o.Name == object.HellGate ||
o.Name == object.SiegeHellGate
} }
func (o Object) IsChest() bool { func (o Object) IsChest() bool {

View File

@@ -28,6 +28,17 @@ func (i Stats) FindStat(id ID, layer int) (Data, bool) {
return Data{}, false return Data{}, false
} }
func (i Stats) FindStatIndex(id ID, layer int) (int, bool) {
for idx, s := range i {
if s.ID == id && s.Layer == layer {
return idx, true
}
}
return 0, false
}
func (s ID) String() string { func (s ID) String() string {
return StringStats[s] return StringStats[s]
} }

View File

@@ -0,0 +1,279 @@
package memory
import (
"encoding/binary"
"errors"
"fmt"
"sync"
"syscall"
"unsafe"
"golang.org/x/sys/windows"
)
type objectAttributes struct {
Length uint32
RootDirectory windows.Handle
ObjectName uintptr
Attributes uint32
SecurityDescriptor uintptr
SecurityQualityOfService uintptr
}
type clientID struct {
UniqueProcess uintptr
UniqueThread uintptr
}
var (
dsInitOnce sync.Once
dsInitErr error
ntOpenProcessStub uintptr
ntReadVMStub uintptr
ntWriteVMStub uintptr
ntCloseStub uintptr
ntAllocVMStub uintptr
ntProtectVMStub uintptr
ntFreeVMStub uintptr
)
func initDirectSyscalls() {
var err error
ntOpenProcessStub, err = buildNtStub("NtOpenProcess")
if err != nil {
dsInitErr = err
return
}
ntReadVMStub, err = buildNtStub("NtReadVirtualMemory")
if err != nil {
dsInitErr = err
return
}
ntWriteVMStub, err = buildNtStub("NtWriteVirtualMemory")
if err != nil {
dsInitErr = err
return
}
ntCloseStub, err = buildNtStub("NtClose")
if err != nil {
dsInitErr = err
return
}
ntAllocVMStub, err = buildNtStub("NtAllocateVirtualMemory")
if err != nil {
dsInitErr = err
return
}
ntProtectVMStub, err = buildNtStub("NtProtectVirtualMemory")
if err != nil {
dsInitErr = err
return
}
ntFreeVMStub, err = buildNtStub("NtFreeVirtualMemory")
if err != nil {
dsInitErr = err
return
}
}
func ensureDirectSyscalls() error {
dsInitOnce.Do(initDirectSyscalls)
return dsInitErr
}
func buildNtStub(name string) (uintptr, error) {
ntdll := windows.NewLazySystemDLL("ntdll.dll")
proc := ntdll.NewProc(name)
if err := ntdll.Load(); err != nil {
return 0, fmt.Errorf("load ntdll.dll: %w", err)
}
addr := proc.Addr()
if addr == 0 {
return 0, fmt.Errorf("%s address is zero", name)
}
sysID, err := extractSysID(addr)
if err != nil {
return 0, fmt.Errorf("extract %s sysid: %w", name, err)
}
return makeSyscallStub(sysID)
}
func extractSysID(procAddr uintptr) (uint32, error) {
b := unsafe.Slice((*byte)(unsafe.Pointer(procAddr)), 64)
if len(b) < 11 {
return 0, errors.New("short Nt stub")
}
if b[0] != 0x4C || b[1] != 0x8B || b[2] != 0xD1 || b[3] != 0xB8 {
return 0, errors.New("unexpected Nt stub prefix")
}
found := false
for i := 0; i+2 < len(b); i++ {
if b[i] == 0x0F && b[i+1] == 0x05 && b[i+2] == 0xC3 {
found = true
break
}
}
if !found {
return 0, errors.New("syscall;ret not found")
}
return binary.LittleEndian.Uint32(b[4:8]), nil
}
func makeSyscallStub(sysID uint32) (uintptr, error) {
code := []byte{0x4C, 0x8B, 0xD1, 0xB8, 0, 0, 0, 0, 0x0F, 0x05, 0xC3}
binary.LittleEndian.PutUint32(code[4:8], sysID)
mem, err := windows.VirtualAlloc(0, uintptr(len(code)), windows.MEM_COMMIT|windows.MEM_RESERVE, windows.PAGE_EXECUTE_READWRITE)
if err != nil {
return 0, err
}
copy(unsafe.Slice((*byte)(unsafe.Pointer(mem)), len(code)), code)
return mem, nil
}
func ntStatusErr(op string, status uintptr) error {
return fmt.Errorf("%s failed: NTSTATUS=0x%08X", op, uint32(status))
}
func ntOpenProcess(pid uint32, access uint32) (windows.Handle, error) {
if err := ensureDirectSyscalls(); err != nil {
return 0, err
}
var h windows.Handle
oa := objectAttributes{Length: uint32(unsafe.Sizeof(objectAttributes{}))}
cid := clientID{UniqueProcess: uintptr(pid)}
status, _, _ := syscall.SyscallN(
ntOpenProcessStub,
uintptr(unsafe.Pointer(&h)),
uintptr(access),
uintptr(unsafe.Pointer(&oa)),
uintptr(unsafe.Pointer(&cid)),
)
if status != 0 {
return 0, ntStatusErr("NtOpenProcess", status)
}
return h, nil
}
func ntReadProcessMemory(handle windows.Handle, address uintptr, out []byte) error {
if len(out) == 0 {
return nil
}
if err := ensureDirectSyscalls(); err != nil {
return err
}
var bytesRead uintptr
status, _, _ := syscall.SyscallN(
ntReadVMStub,
uintptr(handle),
address,
uintptr(unsafe.Pointer(&out[0])),
uintptr(len(out)),
uintptr(unsafe.Pointer(&bytesRead)),
)
if status != 0 {
return ntStatusErr("NtReadVirtualMemory", status)
}
return nil
}
func ntWriteProcessMemory(handle windows.Handle, address uintptr, in []byte) error {
if len(in) == 0 {
return nil
}
if err := ensureDirectSyscalls(); err != nil {
return err
}
var bytesWritten uintptr
status, _, _ := syscall.SyscallN(
ntWriteVMStub,
uintptr(handle),
address,
uintptr(unsafe.Pointer(&in[0])),
uintptr(len(in)),
uintptr(unsafe.Pointer(&bytesWritten)),
)
if status != 0 {
return ntStatusErr("NtWriteVirtualMemory", status)
}
return nil
}
func ntCloseHandle(handle windows.Handle) error {
if handle == 0 {
return nil
}
if err := ensureDirectSyscalls(); err != nil {
return err
}
status, _, _ := syscall.SyscallN(ntCloseStub, uintptr(handle))
if status != 0 {
return ntStatusErr("NtClose", status)
}
return nil
}
func ntAllocateVirtualMemory(handle windows.Handle, size uintptr, allocType, protect uint32) (uintptr, error) {
if err := ensureDirectSyscalls(); err != nil {
return 0, err
}
base := uintptr(0)
regionSize := size
status, _, _ := syscall.SyscallN(
ntAllocVMStub,
uintptr(handle),
uintptr(unsafe.Pointer(&base)),
0,
uintptr(unsafe.Pointer(&regionSize)),
uintptr(allocType),
uintptr(protect),
)
if status != 0 {
return 0, ntStatusErr("NtAllocateVirtualMemory", status)
}
return base, nil
}
func ntProtectVirtualMemory(handle windows.Handle, address uintptr, size uintptr, newProtect uint32) (uint32, error) {
if err := ensureDirectSyscalls(); err != nil {
return 0, err
}
base := address
regionSize := size
var oldProtect uint32
status, _, _ := syscall.SyscallN(
ntProtectVMStub,
uintptr(handle),
uintptr(unsafe.Pointer(&base)),
uintptr(unsafe.Pointer(&regionSize)),
uintptr(newProtect),
uintptr(unsafe.Pointer(&oldProtect)),
)
if status != 0 {
return 0, ntStatusErr("NtProtectVirtualMemory", status)
}
return oldProtect, nil
}
func ntFreeVirtualMemory(handle windows.Handle, address uintptr, freeType uint32) error {
if address == 0 {
return nil
}
if err := ensureDirectSyscalls(); err != nil {
return err
}
base := address
regionSize := uintptr(0) // required for MEM_RELEASE
status, _, _ := syscall.SyscallN(
ntFreeVMStub,
uintptr(handle),
uintptr(unsafe.Pointer(&base)),
uintptr(unsafe.Pointer(&regionSize)),
uintptr(freeType),
)
if status != 0 {
return ntStatusErr("NtFreeVirtualMemory", status)
}
return nil
}

View File

@@ -82,12 +82,16 @@ func (gd *GameReader) GetData() data.Data {
gd.monstersLastUpdate = now gd.monstersLastUpdate = now
} }
// Always update other critical data
corpseUnit := rawPlayerUnits.GetCorpse()
roster := gd.getRoster(rawPlayerUnits)
// Conditionally update inventory 500ms // Conditionally update inventory 500ms
// Except when hovering over an item // Except when hovering over an item
inventory := gd.cachedInventory inventory := gd.cachedInventory
if now.Sub(gd.inventoryLastUpdate) > 250*time.Millisecond || if now.Sub(gd.inventoryLastUpdate) > 250*time.Millisecond ||
(hover.IsHovered && hover.UnitType == 4) { // 4 = Item type (hover.IsHovered && hover.UnitType == 4) { // 4 = Item type
inventory = gd.Inventory(rawPlayerUnits, hover) inventory = gd.Inventory(rawPlayerUnits, len(roster) > 1, hover)
gd.cachedInventory = inventory gd.cachedInventory = inventory
gd.inventoryLastUpdate = now gd.inventoryLastUpdate = now
} }
@@ -100,9 +104,6 @@ func (gd *GameReader) GetData() data.Data {
gd.objectsLastUpdate = now gd.objectsLastUpdate = now
} }
// Always update other critical data
corpseUnit := rawPlayerUnits.GetCorpse()
roster := gd.getRoster(rawPlayerUnits)
openMenus := gd.OpenMenus() openMenus := gd.OpenMenus()
// Quests // Quests
@@ -155,8 +156,9 @@ func (gd *GameReader) GetData() data.Data {
func (gd *GameReader) GetInventory() data.Inventory { func (gd *GameReader) GetInventory() data.Inventory {
rawPlayerUnits := gd.GetRawPlayerUnits() rawPlayerUnits := gd.GetRawPlayerUnits()
roster := gd.getRoster(rawPlayerUnits)
hover := gd.HoveredData() hover := gd.HoveredData()
return gd.Inventory(rawPlayerUnits, hover) return gd.Inventory(rawPlayerUnits, len(roster) > 1, hover)
} }
func (gd *GameReader) InGame() bool { func (gd *GameReader) InGame() bool {

View File

@@ -13,7 +13,7 @@ import (
"github.com/hectorgimenez/d2go/pkg/utils" "github.com/hectorgimenez/d2go/pkg/utils"
) )
func (gd *GameReader) Inventory(rawPlayerUnits RawPlayerUnits, hover data.HoverData) data.Inventory { func (gd *GameReader) Inventory(rawPlayerUnits RawPlayerUnits, partyMode bool, hover data.HoverData) data.Inventory {
mainPlayer := rawPlayerUnits.GetMainPlayer() mainPlayer := rawPlayerUnits.GetMainPlayer()
baseAddr := gd.Process.moduleBaseAddressPtr + gd.offset.UnitTable + (4 * 1024) baseAddr := gd.Process.moduleBaseAddressPtr + gd.offset.UnitTable + (4 * 1024)
unitTableBuffer := gd.Process.ReadBytesFromMemory(baseAddr, 128*8) unitTableBuffer := gd.Process.ReadBytesFromMemory(baseAddr, 128*8)
@@ -275,7 +275,7 @@ func (gd *GameReader) Inventory(rawPlayerUnits RawPlayerUnits, hover data.HoverD
// In party mode / current memory layout: // In party mode / current memory layout:
// shared stash page 1 belongs to the current character. // shared stash page 1 belongs to the current character.
// Other pages are other shared stash containers and should keep raw owner. // Other pages are other shared stash containers and should keep raw owner.
if page == 1 { if !partyMode || page == 1 {
itm.Owner = mainPlayer.UnitID itm.Owner = mainPlayer.UnitID
} }
@@ -493,8 +493,13 @@ func (gd *GameReader) Inventory(rawPlayerUnits RawPlayerUnits, hover data.HoverD
} }
} }
// Build final inventory type itemDistance struct {
inventory.AllItems = make([]data.Item, len(allItems)) item *data.Item
distance int
}
// Finalize items and retain the distance with the item that owns it.
sortedItems := make([]itemDistance, len(allItems))
for i, itm := range allItems { for i, itm := range allItems {
baseDesc := itm.Desc() baseDesc := itm.Desc()
maxReq := calculateItemLevelReq(itm, baseDesc) maxReq := calculateItemLevelReq(itm, baseDesc)
@@ -529,20 +534,19 @@ func (gd *GameReader) Inventory(rawPlayerUnits RawPlayerUnits, hover data.HoverD
break break
} }
} }
inventory.AllItems[i] = *itm sortedItems[i] = itemDistance{
item: itm,
distance: utils.DistanceFromPoint(mainPlayer.Position, itm.Position),
}
} }
// Sort items by distance if needed, using pre-calculated distances sort.SliceStable(sortedItems, func(i, j int) bool {
if len(inventory.AllItems) > 0 { return sortedItems[i].distance < sortedItems[j].distance
// Pre-calculate distances to avoid recalculating in sort comparisons })
distances := make([]int, len(inventory.AllItems))
for i, invItem := range inventory.AllItems {
distances[i] = utils.DistanceFromPoint(mainPlayer.Position, invItem.Position)
}
sort.SliceStable(inventory.AllItems, func(i, j int) bool { inventory.AllItems = make([]data.Item, len(sortedItems))
return distances[i] < distances[j] for i, itm := range sortedItems {
}) inventory.AllItems[i] = *itm.item
} }
inventory.Belt = belt inventory.Belt = belt
@@ -616,8 +620,8 @@ func (gd *GameReader) getItemStats(statsListExPtr uintptr) (stat.Stats, stat.Sta
for _, mStat := range modifierBaseStats { for _, mStat := range modifierBaseStats {
// Update fullStats // Update fullStats
if existingStat, found := fullStats.FindStat(mStat.ID, mStat.Layer); found { if idx, found := fullStats.FindStatIndex(mStat.ID, mStat.Layer); found {
existingStat.Value = mStat.Value fullStats[idx].Value = mStat.Value
} else { } else {
fullStats = append(fullStats, mStat) fullStats = append(fullStats, mStat)
} }

View File

@@ -62,7 +62,7 @@ func calculateOffsets(_ *Process) Offset {
questInfoOffset := uintptr(0x1ED5DB8) questInfoOffset := uintptr(0x1ED5DB8)
// Terror Zones // Terror Zones
tzOffset := uintptr(0x25B5578) tzOffset := uintptr(0x25C3B50)
// Ping // Ping
pingOffset := uintptr(0x1E0C508) pingOffset := uintptr(0x1E0C508)

View File

@@ -36,7 +36,7 @@ func NewProcess() (*Process, error) {
return nil, err return nil, err
} }
h, err := windows.OpenProcess(0x0010, false, module.ProcessID) h, err := ntOpenProcess(module.ProcessID, 0x0010)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -55,7 +55,7 @@ func NewProcessForPID(pid uint32) (*Process, error) {
return nil, errors.New("no module found for the specified PID") return nil, errors.New("no module found for the specified PID")
} }
h, err := windows.OpenProcess(0x0010, false, module.ProcessID) h, err := ntOpenProcess(module.ProcessID, 0x0010)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -69,7 +69,7 @@ func NewProcessForPID(pid uint32) (*Process, error) {
} }
func (p *Process) Close() error { func (p *Process) Close() error {
return windows.CloseHandle(p.handler) return ntCloseHandle(p.handler)
} }
// ModuleBaseAddress returns the base address of the D2R module. // ModuleBaseAddress returns the base address of the D2R module.
@@ -138,7 +138,7 @@ func ReadMemoryChunked(handle windows.Handle, baseAddress uintptr, size uint32)
} }
// Try to read, but don't fail if it's protected // Try to read, but don't fail if it's protected
err := windows.ReadProcessMemory(handle, address, &data[offset], chunkSize, nil) err := ntReadProcessMemory(handle, address, data[offset:offset+chunkSize])
if err != nil { if err != nil {
failedReads++ failedReads++
// Fill with zeros and continue (pattern matching will fail gracefully) // Fill with zeros and continue (pattern matching will fail gracefully)
@@ -155,7 +155,7 @@ func ReadMemoryChunked(handle windows.Handle, baseAddress uintptr, size uint32)
func (p *Process) ReadBytesFromMemory(address uintptr, size uint) []byte { func (p *Process) ReadBytesFromMemory(address uintptr, size uint) []byte {
var data = make([]byte, size) var data = make([]byte, size)
windows.ReadProcessMemory(p.handler, address, &data[0], uintptr(size), nil) _ = ntReadProcessMemory(p.handler, address, data)
return data return data
} }
@@ -275,11 +275,11 @@ type ModuleInfo struct {
} }
func GetProcessModules(processID uint32) ([]ModuleInfo, error) { func GetProcessModules(processID uint32) ([]ModuleInfo, error) {
hProcess, err := windows.OpenProcess(windows.PROCESS_QUERY_INFORMATION|windows.PROCESS_VM_READ, false, processID) hProcess, err := ntOpenProcess(processID, windows.PROCESS_QUERY_INFORMATION|windows.PROCESS_VM_READ)
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer windows.CloseHandle(hProcess) defer ntCloseHandle(hProcess)
var modules [1024]windows.Handle var modules [1024]windows.Handle
var needed uint32 var needed uint32
@@ -323,7 +323,7 @@ func (p *Process) ReadPointer(address uintptr, size int) (uintptr, error) {
} }
func (p *Process) ReadIntoBuffer(address uintptr, buffer []byte) error { func (p *Process) ReadIntoBuffer(address uintptr, buffer []byte) error {
return windows.ReadProcessMemory(p.handler, address, &buffer[0], uintptr(len(buffer)), nil) return ntReadProcessMemory(p.handler, address, buffer)
} }
// ReadWidgetContainer reads the WidgetContainer structure. // ReadWidgetContainer reads the WidgetContainer structure.

View File

@@ -38,21 +38,14 @@ var (
} }
kernel32 = windows.NewLazySystemDLL("kernel32.dll") kernel32 = windows.NewLazySystemDLL("kernel32.dll")
procVirtualAllocEx = kernel32.NewProc("VirtualAllocEx")
procVirtualFreeEx = kernel32.NewProc("VirtualFreeEx")
procQueueUserAPC = kernel32.NewProc("QueueUserAPC") procQueueUserAPC = kernel32.NewProc("QueueUserAPC")
procSuspendThread = kernel32.NewProc("SuspendThread") procSuspendThread = kernel32.NewProc("SuspendThread")
procResumeThread = kernel32.NewProc("ResumeThread") procResumeThread = kernel32.NewProc("ResumeThread")
procGetExitCodeThread = kernel32.NewProc("GetExitCodeThread") procGetExitCodeThread = kernel32.NewProc("GetExitCodeThread")
procGetThreadTimes = kernel32.NewProc("GetThreadTimes") procGetThreadTimes = kernel32.NewProc("GetThreadTimes")
procVirtualProtectEx = kernel32.NewProc("VirtualProtectEx")
procSetProcessValidCallTargets = kernel32.NewProc("SetProcessValidCallTargets") procSetProcessValidCallTargets = kernel32.NewProc("SetProcessValidCallTargets")
procCreateRemoteThread = kernel32.NewProc("CreateRemoteThread") procCreateRemoteThread = kernel32.NewProc("CreateRemoteThread")
ntdll = windows.NewLazySystemDLL("ntdll.dll")
procNtGetContextThread = ntdll.NewProc("NtGetContextThread")
procNtSetContextThread = ntdll.NewProc("NtSetContextThread")
d2gsCachedFn uintptr d2gsCachedFn uintptr
d2gsCacheMu sync.RWMutex d2gsCacheMu sync.RWMutex
d2gsCachePID uint32 d2gsCachePID uint32
@@ -143,7 +136,7 @@ func (s *sendPacketState) ensureHandle(pid uint32) (windows.Handle, error) {
virtualFreeEx(s.handle, s.stub) virtualFreeEx(s.handle, s.stub)
} }
windows.CloseHandle(s.handle) _ = ntCloseHandle(s.handle)
s.handle = 0 s.handle = 0
s.processPID = 0 s.processPID = 0
s.stub = 0 s.stub = 0
@@ -153,7 +146,7 @@ func (s *sendPacketState) ensureHandle(pid uint32) (windows.Handle, error) {
s.fn = 0 s.fn = 0
} }
h, err := windows.OpenProcess(sendPacketProcessAccess, false, pid) h, err := ntOpenProcess(pid, sendPacketProcessAccess)
if err != nil { if err != nil {
return 0, fmt.Errorf("open process %d: %w", pid, err) return 0, fmt.Errorf("open process %d: %w", pid, err)
} }
@@ -305,7 +298,7 @@ func (s *sendPacketState) Cleanup() error {
// Close thread handle - the main thread may change between games // Close thread handle - the main thread may change between games
if s.thread != 0 { if s.thread != 0 {
windows.CloseHandle(s.thread) _ = ntCloseHandle(s.thread)
s.thread = 0 s.thread = 0
s.threadID = 0 s.threadID = 0
} }
@@ -343,12 +336,12 @@ func (s *sendPacketState) ensureThreadHandle(p *Process) (windows.Handle, error)
} }
} }
// Thread is invalid, close and reset // Thread is invalid, close and reset
windows.CloseHandle(s.thread) _ = ntCloseHandle(s.thread)
s.thread = 0 s.thread = 0
s.threadID = 0 s.threadID = 0
s.processPID = 0 s.processPID = 0
} else if s.thread != 0 && s.processPID != p.pid { } else if s.thread != 0 && s.processPID != p.pid {
windows.CloseHandle(s.thread) _ = ntCloseHandle(s.thread)
s.thread = 0 s.thread = 0
s.threadID = 0 s.threadID = 0
s.processPID = 0 s.processPID = 0
@@ -366,7 +359,7 @@ func (s *sendPacketState) ensureThreadHandle(p *Process) (windows.Handle, error)
// Validate the newly opened thread belongs to the correct process // Validate the newly opened thread belongs to the correct process
if err := validateThreadBelongsToProcess(handle, p.pid); err != nil { if err := validateThreadBelongsToProcess(handle, p.pid); err != nil {
windows.CloseHandle(handle) _ = ntCloseHandle(handle)
return 0, fmt.Errorf("thread %d validation failed: %w", threadID, err) return 0, fmt.Errorf("thread %d validation failed: %w", threadID, err)
} }
@@ -509,7 +502,7 @@ func findMainThreadID(pid uint32) (uint32, error) {
if err != nil { if err != nil {
return 0, fmt.Errorf("CreateToolhelp32Snapshot: %w", err) return 0, fmt.Errorf("CreateToolhelp32Snapshot: %w", err)
} }
defer windows.CloseHandle(snapshot) defer ntCloseHandle(snapshot)
var entry windows.ThreadEntry32 var entry windows.ThreadEntry32
entry.Size = uint32(unsafe.Sizeof(entry)) entry.Size = uint32(unsafe.Sizeof(entry))
@@ -526,7 +519,7 @@ func findMainThreadID(pid uint32) (uint32, error) {
thread, err := windows.OpenThread(sendPacketThreadAccess, false, entry.ThreadID) thread, err := windows.OpenThread(sendPacketThreadAccess, false, entry.ThreadID)
if err == nil { if err == nil {
creationTime, err := getThreadCreationTime(thread) creationTime, err := getThreadCreationTime(thread)
windows.CloseHandle(thread) _ = ntCloseHandle(thread)
if err == nil && creationTime < earliestCreationTime { if err == nil && creationTime < earliestCreationTime {
earliestCreationTime = creationTime earliestCreationTime = creationTime
@@ -866,7 +859,7 @@ func (p *Process) waitForPacketCompletion(handle windows.Handle, state *sendPack
select { select {
case <-timeout: case <-timeout:
// Try to read status one last time before failing // Try to read status one last time before failing
if err := windows.ReadProcessMemory(handle, state.meta+sendPacketStatusOffset, &statusBuf[0], 4, nil); err == nil { if err := ntReadProcessMemory(handle, state.meta+sendPacketStatusOffset, statusBuf[:]); err == nil {
status := binary.LittleEndian.Uint32(statusBuf[:]) status := binary.LittleEndian.Uint32(statusBuf[:])
if status == 1 { if status == 1 {
return nil return nil
@@ -874,7 +867,7 @@ func (p *Process) waitForPacketCompletion(handle windows.Handle, state *sendPack
} }
return fmt.Errorf("packet send timeout: APC not processed within %dms", timeoutMs) return fmt.Errorf("packet send timeout: APC not processed within %dms", timeoutMs)
case <-ticker.C: case <-ticker.C:
if err := windows.ReadProcessMemory(handle, state.meta+sendPacketStatusOffset, &statusBuf[0], 4, nil); err != nil { if err := ntReadProcessMemory(handle, state.meta+sendPacketStatusOffset, statusBuf[:]); err != nil {
continue continue
} }
status := binary.LittleEndian.Uint32(statusBuf[:]) status := binary.LittleEndian.Uint32(statusBuf[:])
@@ -892,74 +885,26 @@ func writeRemoteMemory(handle windows.Handle, address uintptr, data []byte) erro
if len(data) == 0 { if len(data) == 0 {
return nil return nil
} }
return windows.WriteProcessMemory(handle, address, &data[0], uintptr(len(data)), nil) return ntWriteProcessMemory(handle, address, data)
} }
func virtualAllocEx(handle windows.Handle, size uintptr, protect uint32) (uintptr, error) { func virtualAllocEx(handle windows.Handle, size uintptr, protect uint32) (uintptr, error) {
if size == 0 { if size == 0 {
return 0, errors.New("allocation size cannot be zero") return 0, errors.New("allocation size cannot be zero")
} }
return ntAllocateVirtualMemory(handle, size, windows.MEM_COMMIT|windows.MEM_RESERVE, protect)
addr, _, err := procVirtualAllocEx.Call(
uintptr(handle),
0,
size,
windows.MEM_COMMIT|windows.MEM_RESERVE,
uintptr(protect),
)
if addr == 0 {
if err != nil {
return 0, err
}
return 0, errors.New("VirtualAllocEx failed")
}
return addr, nil
} }
func virtualFreeEx(handle windows.Handle, address uintptr) error { func virtualFreeEx(handle windows.Handle, address uintptr) error {
if address == 0 { return ntFreeVirtualMemory(handle, address, windows.MEM_RELEASE)
return nil
}
ret, _, err := procVirtualFreeEx.Call(
uintptr(handle),
address,
0,
windows.MEM_RELEASE,
)
if ret == 0 {
if err != nil {
return err
}
return errors.New("VirtualFreeEx failed")
}
return nil
} }
func virtualProtectEx(handle windows.Handle, address uintptr, size uintptr, protect uint32) error { func virtualProtectEx(handle windows.Handle, address uintptr, size uintptr, protect uint32) error {
if address == 0 || size == 0 { if address == 0 || size == 0 {
return errors.New("attempt to protect invalid region") return errors.New("attempt to protect invalid region")
} }
_, err := ntProtectVirtualMemory(handle, address, size, protect)
if err := procVirtualProtectEx.Find(); err != nil { return err
return nil
}
var oldProtect uint32
ret, _, err := procVirtualProtectEx.Call(
uintptr(handle),
address,
size,
uintptr(protect),
uintptr(unsafe.Pointer(&oldProtect)),
)
if ret == 0 {
if err != nil {
return fmt.Errorf("VirtualProtectEx: %w", err)
}
return errors.New("VirtualProtectEx failed")
}
return nil
} }
func markCallTargetValid(handle windows.Handle, address uintptr, size uintptr) error { func markCallTargetValid(handle windows.Handle, address uintptr, size uintptr) error {
@@ -1078,26 +1023,3 @@ const CONTEXT_AMD64 = 0x00100000
const CONTEXT_CONTROL = CONTEXT_AMD64 | 0x0001 const CONTEXT_CONTROL = CONTEXT_AMD64 | 0x0001
const CONTEXT_INTEGER = CONTEXT_AMD64 | 0x0002 const CONTEXT_INTEGER = CONTEXT_AMD64 | 0x0002
const CONTEXT_FULL = CONTEXT_CONTROL | CONTEXT_INTEGER const CONTEXT_FULL = CONTEXT_CONTROL | CONTEXT_INTEGER
func getThreadContext(threadHandle windows.Handle, ctx *CONTEXT64) error {
ctx.ContextFlags = CONTEXT_FULL
ret, _, err := procNtGetContextThread.Call(
uintptr(threadHandle),
uintptr(unsafe.Pointer(ctx)),
)
if ret != 0 {
return fmt.Errorf("NtGetContextThread failed: 0x%X, %w", ret, err)
}
return nil
}
func setThreadContext(threadHandle windows.Handle, ctx *CONTEXT64) error {
ret, _, err := procNtSetContextThread.Call(
uintptr(threadHandle),
uintptr(unsafe.Pointer(ctx)),
)
if ret != 0 {
return fmt.Errorf("NtSetContextThread failed: 0x%X, %w", ret, err)
}
return nil
}

View File

@@ -450,6 +450,11 @@ func (r Rule) Evaluate(it data.Item) (RuleResult, error) {
// Special handling for stats not found // Special handling for stats not found
if !statFound { if !statFound {
if statName == "allres" {
stage2Props[statName] = evaluateAllRes(it)
continue
}
isResistStat := strings.Contains(statName, "resist") isResistStat := strings.Contains(statName, "resist")
// Special case: Pure resist-sum rules (like sunders) should fail early if no resists exist // Special case: Pure resist-sum rules (like sunders) should fail early if no resists exist
// But mixed rules (resists OR other stats) should continue to check other conditions // But mixed rules (resists OR other stats) should continue to check other conditions
@@ -579,6 +584,28 @@ func evaluateSkillTabSum(it data.Item) int {
return evaluatePositiveStatSum(it, stat.AddSkillTab, maxSkillTabLayer) return evaluatePositiveStatSum(it, stat.AddSkillTab, maxSkillTabLayer)
} }
func evaluateAllRes(it data.Item) int {
resistStats := []stat.ID{
stat.FireResist,
stat.LightningResist,
stat.ColdResist,
stat.PoisonResist,
}
lowest := 0
for idx, statID := range resistStats {
itemStat, found := it.FindStat(statID, 0)
if !found {
return 0
}
if idx == 0 || itemStat.Value < lowest {
lowest = itemStat.Value
}
}
return lowest
}
const aggregateLayerFastPathLimit = 64 const aggregateLayerFastPathLimit = 64
func evaluatePositiveStatSum(it data.Item, statID stat.ID, maxLayer int) int { func evaluatePositiveStatSum(it data.Item, statID stat.ID, maxLayer int) int {