diff --git a/pkg/memory/game_reader.go b/pkg/memory/game_reader.go index 7d3ea40..859228c 100644 --- a/pkg/memory/game_reader.go +++ b/pkg/memory/game_reader.go @@ -14,7 +14,7 @@ import ( type GameReader struct { offset Offset - Process + *Process monstersLastUpdate time.Time inventoryLastUpdate time.Time @@ -39,7 +39,7 @@ var WidgetStateFlags = map[string]uint64{ "WeaponSwap": 0xF2D7CF8E9CC08212, } -func NewGameReader(process Process) *GameReader { +func NewGameReader(process *Process) *GameReader { return &GameReader{ offset: calculateOffsets(process), Process: process, diff --git a/pkg/memory/offset.go b/pkg/memory/offset.go index 8056b6d..9f5be86 100644 --- a/pkg/memory/offset.go +++ b/pkg/memory/offset.go @@ -19,7 +19,7 @@ type Offset struct { KeyBindingsSkillsOffset uintptr } -func calculateOffsets(process Process) Offset { +func calculateOffsets(process *Process) Offset { // ignoring errors, always best practices memory, _ := process.getProcessMemory() diff --git a/pkg/memory/process.go b/pkg/memory/process.go index 184e83b..e6e5410 100644 --- a/pkg/memory/process.go +++ b/pkg/memory/process.go @@ -5,6 +5,7 @@ import ( "encoding/binary" "errors" "strings" + "sync" "syscall" "unsafe" @@ -18,27 +19,29 @@ type Process struct { pid uint32 moduleBaseAddressPtr uintptr moduleBaseSize uint32 + sendPacket *sendPacketState + sendPacketMu sync.Mutex } const ( - Int8 = 1 // signed 8-bit integer - Int16 = 2 // signed 16-bit integer - Int32 = 4 // signed 32-bit integer - Int64 = 8 // signed 64-bit integer + Int8 = 1 + Int16 = 2 + Int32 = 4 + Int64 = 8 ) -func NewProcess() (Process, error) { +func NewProcess() (*Process, error) { module, err := getGameModule() if err != nil { - return Process{}, err + return nil, err } h, err := windows.OpenProcess(0x0010, false, module.ProcessID) if err != nil { - return Process{}, err + return nil, err } - return Process{ + return &Process{ handler: h, pid: module.ProcessID, moduleBaseAddressPtr: module.ModuleBaseAddress, @@ -46,18 +49,18 @@ func NewProcess() (Process, error) { }, nil } -func NewProcessForPID(pid uint32) (Process, error) { +func NewProcessForPID(pid uint32) (*Process, error) { module, found := getMainModule(pid) if !found { - return Process{}, 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) if err != nil { - return Process{}, err + return nil, err } - return Process{ + return &Process{ handler: h, pid: module.ProcessID, moduleBaseAddressPtr: module.ModuleBaseAddress, @@ -65,7 +68,7 @@ func NewProcessForPID(pid uint32) (Process, error) { }, nil } -func (p Process) Close() error { +func (p *Process) Close() error { return windows.CloseHandle(p.handler) } @@ -101,7 +104,7 @@ func getMainModule(pid uint32) (ModuleInfo, bool) { return ModuleInfo{}, false } -func (p Process) getProcessMemory() ([]byte, error) { +func (p *Process) getProcessMemory() ([]byte, error) { var data = make([]byte, p.moduleBaseSize) err := windows.ReadProcessMemory(p.handler, p.moduleBaseAddressPtr, &data[0], uintptr(p.moduleBaseSize), nil) if err != nil { @@ -111,7 +114,7 @@ func (p Process) getProcessMemory() ([]byte, error) { return data, nil } -func (p Process) ReadBytesFromMemory(address uintptr, size uint) []byte { +func (p *Process) ReadBytesFromMemory(address uintptr, size uint) []byte { var data = make([]byte, size) windows.ReadProcessMemory(p.handler, address, &data[0], uintptr(size), nil) @@ -127,7 +130,7 @@ const ( Uint64 = 8 ) -func (p Process) ReadUInt(address uintptr, size IntType) uint { +func (p *Process) ReadUInt(address uintptr, size IntType) uint { bytes := p.ReadBytesFromMemory(address, uint(size)) return bytesToUint(bytes, size) @@ -168,7 +171,7 @@ func bytesToInt(bytes []byte, size IntType) int { return 0 } -func (p Process) ReadStringFromMemory(address uintptr, size uint) string { +func (p *Process) ReadStringFromMemory(address uintptr, size uint) string { if size == 0 { for i := 1; true; i++ { data := p.ReadBytesFromMemory(address, uint(i)) @@ -181,7 +184,7 @@ func (p Process) ReadStringFromMemory(address uintptr, size uint) string { return string(bytes.Trim(p.ReadBytesFromMemory(address, size), "\x00")) } -func (p Process) findPattern(memory []byte, pattern, mask string) int { +func (p *Process) findPattern(memory []byte, pattern, mask string) int { patternLength := len(pattern) for i := 0; i < int(p.moduleBaseSize)-patternLength; i++ { found := true @@ -200,7 +203,7 @@ func (p Process) findPattern(memory []byte, pattern, mask string) int { return 0 } -func (p Process) FindPattern(memory []byte, pattern, mask string) uintptr { +func (p *Process) FindPattern(memory []byte, pattern, mask string) uintptr { if offset := p.findPattern(memory, pattern, mask); offset != 0 { return p.moduleBaseAddressPtr + uintptr(offset) } @@ -208,7 +211,7 @@ func (p Process) FindPattern(memory []byte, pattern, mask string) uintptr { return 0 } -func (p Process) FindPatternByOperand(memory []byte, pattern, mask string) uintptr { +func (p *Process) FindPatternByOperand(memory []byte, pattern, mask string) uintptr { if offset := p.findPattern(memory, pattern, mask); offset != 0 { // Adjust the address based on the operand value operandAddress := p.moduleBaseAddressPtr + uintptr(offset) @@ -220,7 +223,7 @@ func (p Process) FindPatternByOperand(memory []byte, pattern, mask string) uintp return 0 } -func (p Process) GetPID() uint32 { +func (p *Process) GetPID() uint32 { return p.pid } @@ -280,7 +283,7 @@ func (p *Process) ReadPointer(address uintptr, size int) (uintptr, error) { return uintptr(*(*uint64)(unsafe.Pointer(&buffer[0]))), nil } -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) } diff --git a/pkg/memory/send_packet.go b/pkg/memory/send_packet.go new file mode 100644 index 0000000..fb6e08d --- /dev/null +++ b/pkg/memory/send_packet.go @@ -0,0 +1,633 @@ +package memory + +import ( + "encoding/binary" + "errors" + "fmt" + "log" + "strings" + "sync" + "time" + "unsafe" + + "golang.org/x/sys/windows" +) + +const ( + d2gsSendPacketPattern = "\xE8\x00\x00\x00\x00\x0F\xB6\x85\x00\x00\x00\x00\x48\x03\xF0" + d2gsSendPacketMask = "x????xxx????xxx" + maxPacketSize = 65536 +) + +var ( + sendPacketStubBase = []byte{ + 0x48, 0x89, 0xC8, + 0x49, 0x89, 0xC0, + 0x48, 0x83, 0xEC, 0x28, + 0x48, 0x8B, 0x48, 0x08, + 0x48, 0x8B, 0x50, 0x10, + 0x48, 0x8B, 0x00, + 0xFF, 0xD0, + 0x41, 0xC7, 0x40, 0x18, + 0x01, 0x00, 0x00, 0x00, + 0xB8, 0x01, 0x00, 0x00, 0x00, + 0x48, 0x83, 0xC4, 0x28, + 0xC3, + } + + kernel32 = windows.NewLazySystemDLL("kernel32.dll") + procVirtualAllocEx = kernel32.NewProc("VirtualAllocEx") + procVirtualFreeEx = kernel32.NewProc("VirtualFreeEx") + procQueueUserAPC = kernel32.NewProc("QueueUserAPC") + procSuspendThread = kernel32.NewProc("SuspendThread") + procResumeThread = kernel32.NewProc("ResumeThread") + procGetExitCodeThread = kernel32.NewProc("GetExitCodeThread") + procGetThreadTimes = kernel32.NewProc("GetThreadTimes") + + d2gsCachedFn uintptr + d2gsCacheMu sync.RWMutex + d2gsCachePID uint32 + + metaBufPool = sync.Pool{ + New: func() interface{} { + return new([sendPacketMetaSize]byte) + }, + } +) + +const ( + sendPacketProcessAccess = windows.PROCESS_VM_OPERATION | windows.PROCESS_VM_READ | windows.PROCESS_VM_WRITE | windows.PROCESS_QUERY_INFORMATION + + THREAD_SUSPEND_RESUME = 0x0002 + THREAD_SET_CONTEXT = 0x0010 + THREAD_QUERY_INFORMATION = 0x0040 + sendPacketThreadAccess = THREAD_SET_CONTEXT | THREAD_SUSPEND_RESUME | THREAD_QUERY_INFORMATION + + sendPacketStatusOffset = 24 + sendPacketMetaSize = 32 + + threadStillActive = 259 +) + +type sendPacketState struct { + mu sync.Mutex + handle windows.Handle + stub uintptr + meta uintptr + packet uintptr + packetCap uintptr + fn uintptr + thread windows.Handle + threadID uint32 + processPID uint32 + threadLastValidated time.Time + leakedBuffers []uintptr +} + +func findPatternOffset(memory []byte, pattern, mask string) int { + if len(pattern) != len(mask) { + return -1 + } + patternLength := len(pattern) + limit := len(memory) - patternLength + for i := 0; i <= limit; i++ { + match := true + for j := 0; j < patternLength; j++ { + if mask[j] == 'x' && memory[i+j] != pattern[j] { + match = false + break + } + } + if match { + return i + } + } + return -1 +} + +func (s *sendPacketState) ensureHandle(pid uint32) (windows.Handle, error) { + if s.handle != 0 && s.processPID == pid { + return s.handle, nil + } + + if s.handle != 0 && s.processPID != pid { + if s.packet != 0 { + virtualFreeEx(s.handle, s.packet) + } + if s.meta != 0 { + virtualFreeEx(s.handle, s.meta) + } + if s.stub != 0 { + virtualFreeEx(s.handle, s.stub) + } + + windows.CloseHandle(s.handle) + s.handle = 0 + s.processPID = 0 + s.stub = 0 + s.meta = 0 + s.packet = 0 + s.packetCap = 0 + s.fn = 0 + } + + h, err := windows.OpenProcess(sendPacketProcessAccess, false, pid) + if err != nil { + return 0, fmt.Errorf("open process %d: %w", pid, err) + } + s.handle = h + s.processPID = pid + return h, nil +} + +func (s *sendPacketState) ensureStub(handle windows.Handle) error { + if s.stub != 0 { + return nil + } + + stubSize := uintptr(len(sendPacketStubBase)) + + addr, err := virtualAllocEx(handle, stubSize, windows.PAGE_EXECUTE_READWRITE) + if err != nil { + return fmt.Errorf("allocate remote stub: %w", err) + } + + if err := writeRemoteMemory(handle, addr, sendPacketStubBase); err != nil { + virtualFreeEx(handle, addr) + return fmt.Errorf("write remote stub: %w", err) + } + + s.stub = addr + return nil +} + +func (s *sendPacketState) ensureMeta(handle windows.Handle) error { + if s.meta != 0 { + return nil + } + addr, err := virtualAllocEx(handle, sendPacketMetaSize, windows.PAGE_READWRITE) + if err != nil { + return fmt.Errorf("allocate remote metadata: %w", err) + } + s.meta = addr + return nil +} + +func (s *sendPacketState) ensurePacketBuffer(handle windows.Handle, size uintptr) error { + if size == 0 { + return errors.New("packet size must be greater than zero") + } + + if size > maxPacketSize { + return fmt.Errorf("packet too large: %d bytes (max %d)", size, maxPacketSize) + } + + if size <= s.packetCap && s.packet != 0 { + return nil + } + + allocSize := size + if size < 4096 { + allocSize = 4096 + } else { + allocSize = (size + 4095) &^ 4095 + } + + newPacket, err := virtualAllocEx(handle, allocSize, windows.PAGE_READWRITE) + if err != nil { + return fmt.Errorf("allocate packet buffer of %d bytes: %w", allocSize, err) + } + + if s.packet != 0 { + if err := virtualFreeEx(handle, s.packet); err != nil { + log.Printf("Warning: failed to free old packet buffer at 0x%X: %v", s.packet, err) + s.leakedBuffers = append(s.leakedBuffers, s.packet) + } + } + + s.packet = newPacket + s.packetCap = allocSize + return nil +} + +func (s *sendPacketState) ensureFunction(p *Process) (uintptr, error) { + if s.fn != 0 { + return s.fn, nil + } + + fn, err := p.GetD2GSSendPacketFn() + if err != nil { + return 0, err + } + + if fn == 0 { + return 0, errors.New("D2GS_SendPacket function pointer is null") + } + + s.fn = fn + return fn, nil +} + +func (s *sendPacketState) ensureThreadHandle(p *Process) (windows.Handle, error) { + if s.thread != 0 && s.processPID == p.pid { + if err := validateThreadActive(s.thread); err == nil { + s.threadLastValidated = time.Now() + return s.thread, nil + } + windows.CloseHandle(s.thread) + s.thread = 0 + s.threadID = 0 + s.processPID = 0 + } else if s.thread != 0 && s.processPID != p.pid { + windows.CloseHandle(s.thread) + s.thread = 0 + s.threadID = 0 + s.processPID = 0 + } + + threadID, err := findMainThreadID(p.pid) + if err != nil { + return 0, err + } + + handle, err := windows.OpenThread(sendPacketThreadAccess, false, threadID) + if err != nil { + return 0, fmt.Errorf("open thread %d: %w", threadID, err) + } + + s.thread = handle + s.threadID = threadID + s.processPID = p.pid + s.threadLastValidated = time.Now() + return handle, nil +} + +func validateThreadActive(thread windows.Handle) error { + var exitCode uint32 + ret, _, err := procGetExitCodeThread.Call( + uintptr(thread), + uintptr(unsafe.Pointer(&exitCode)), + ) + if ret == 0 { + if err != nil { + return fmt.Errorf("GetExitCodeThread: %w", err) + } + return errors.New("GetExitCodeThread failed") + } + + if exitCode != threadStillActive { + return fmt.Errorf("thread is not active (exit code: %d)", exitCode) + } + + return nil +} + +func (s *sendPacketState) dispatchAPC(thread windows.Handle, start, parameter uintptr) error { + if thread == 0 { + return errors.New("thread handle is zero") + } + + if err := suspendThread(thread); err != nil { + return err + } + + queued := false + defer func() { + if !queued { + _ = resumeThread(thread) + } + }() + + if err := queueUserAPC(start, thread, parameter); err != nil { + return err + } + queued = true + + if err := resumeThread(thread); err != nil { + queued = false + return err + } + + return nil +} + +func queueUserAPC(start uintptr, thread windows.Handle, parameter uintptr) error { + ret, _, err := procQueueUserAPC.Call(start, uintptr(thread), parameter) + if ret == 0 { + if err != nil { + return fmt.Errorf("QueueUserAPC: %w", err) + } + return errors.New("QueueUserAPC failed") + } + return nil +} + +func suspendThread(thread windows.Handle) error { + ret, _, err := procSuspendThread.Call(uintptr(thread)) + if ret == ^uintptr(0) { + if err != nil { + return fmt.Errorf("SuspendThread: %w", err) + } + return errors.New("SuspendThread failed") + } + return nil +} + +func resumeThread(thread windows.Handle) error { + ret, _, err := procResumeThread.Call(uintptr(thread)) + if ret == ^uintptr(0) { + if err != nil { + return fmt.Errorf("ResumeThread: %w", err) + } + return errors.New("ResumeThread failed") + } + return nil +} + +// getThreadCreationTime gets the creation time of a thread +func getThreadCreationTime(thread windows.Handle) (int64, error) { + var creationTime, exitTime, kernelTime, userTime windows.Filetime + ret, _, err := procGetThreadTimes.Call( + uintptr(thread), + uintptr(unsafe.Pointer(&creationTime)), + uintptr(unsafe.Pointer(&exitTime)), + uintptr(unsafe.Pointer(&kernelTime)), + uintptr(unsafe.Pointer(&userTime)), + ) + if ret == 0 { + if err != nil { + return 0, fmt.Errorf("GetThreadTimes: %w", err) + } + return 0, errors.New("GetThreadTimes failed") + } + + return int64(creationTime.HighDateTime)<<32 | int64(creationTime.LowDateTime), nil +} + +func findMainThreadID(pid uint32) (uint32, error) { + snapshot, err := windows.CreateToolhelp32Snapshot(windows.TH32CS_SNAPTHREAD, 0) + if err != nil { + return 0, fmt.Errorf("CreateToolhelp32Snapshot: %w", err) + } + defer windows.CloseHandle(snapshot) + + var entry windows.ThreadEntry32 + entry.Size = uint32(unsafe.Sizeof(entry)) + if err := windows.Thread32First(snapshot, &entry); err != nil { + return 0, fmt.Errorf("Thread32First: %w", err) + } + + var mainThreadID uint32 + var earliestCreationTime int64 = 0x7FFFFFFFFFFFFFFF + var found bool + + for { + if entry.OwnerProcessID == pid { + thread, err := windows.OpenThread(sendPacketThreadAccess, false, entry.ThreadID) + if err == nil { + creationTime, err := getThreadCreationTime(thread) + windows.CloseHandle(thread) + + if err == nil && creationTime < earliestCreationTime { + earliestCreationTime = creationTime + mainThreadID = entry.ThreadID + found = true + } + } + } + + if err := windows.Thread32Next(snapshot, &entry); err != nil { + if errors.Is(err, windows.ERROR_NO_MORE_FILES) { + break + } + return 0, fmt.Errorf("Thread32Next: %w", err) + } + } + + if !found { + return 0, fmt.Errorf("no threads found for process %d", pid) + } + + return mainThreadID, nil +} + +func validatePatternMatch(memory []byte, offset int) bool { + if offset >= len(memory) || memory[offset] != 0xE8 { + return false + } + + if offset+5 > len(memory) { + return false + } + + if offset+14 < len(memory) { + if memory[offset+5] != 0x0F || memory[offset+6] != 0xB6 { + return false + } + } + + return true +} + +func (p *Process) GetD2GSSendPacketFn() (uintptr, error) { + if p == nil { + return 0, errors.New("process is nil") + } + + if p.handler == 0 { + return 0, errors.New("process handle is invalid") + } + + d2gsCacheMu.RLock() + if d2gsCachedFn != 0 && d2gsCachePID == p.pid { + fn := d2gsCachedFn + d2gsCacheMu.RUnlock() + return fn, nil + } + d2gsCacheMu.RUnlock() + + modules, err := GetProcessModules(p.pid) + if err != nil { + return 0, fmt.Errorf("enumerate modules: %w", err) + } + + for _, module := range modules { + name := strings.ToLower(module.ModuleName) + + if strings.Contains(name, "windows") || strings.Contains(name, "system32") { + continue + } + + if module.ModuleBaseSize == 0 || module.ModuleBaseSize > 100*1024*1024 { + continue + } + + memory := make([]byte, int(module.ModuleBaseSize)) + if err := windows.ReadProcessMemory(p.handler, module.ModuleBaseAddress, &memory[0], uintptr(module.ModuleBaseSize), nil); err != nil { + log.Printf("Warning: failed to read module %s: %v", module.ModuleName, err) + continue + } + + offset := findPatternOffset(memory, d2gsSendPacketPattern, d2gsSendPacketMask) + if offset < 0 { + continue + } + + if !validatePatternMatch(memory, offset) { + log.Printf("Warning: false positive pattern match at offset 0x%X in %s", offset, module.ModuleName) + continue + } + + patternAddr := module.ModuleBaseAddress + uintptr(offset) + relOffset := int32(binary.LittleEndian.Uint32(memory[offset+1 : offset+5])) + absolute := uintptr(int64(patternAddr+5) + int64(relOffset)) + + if absolute == 0 || absolute < module.ModuleBaseAddress { + log.Printf("Warning: invalid computed address 0x%X in %s", absolute, module.ModuleName) + continue + } + + log.Printf("D2GS_SendPacket resolved at 0x%X in module %s", absolute, module.ModuleName) + + d2gsCacheMu.Lock() + d2gsCachedFn = absolute + d2gsCachePID = p.pid + d2gsCacheMu.Unlock() + + return absolute, nil + } + + return 0, errors.New("D2GS_SendPacket pattern not found in any module") +} + +func (p *Process) SendPacket(packet []byte) (err error) { + if p == nil { + return errors.New("process is nil") + } + + defer func() { + if err != nil { + log.Printf("SendPacket(%d bytes) error: %v", len(packet), err) + } + }() + + if len(packet) == 0 { + return errors.New("packet payload cannot be empty") + } + + if len(packet) > maxPacketSize { + return fmt.Errorf("packet too large: %d bytes (max %d)", len(packet), maxPacketSize) + } + + p.sendPacketMu.Lock() + if p.sendPacket == nil { + p.sendPacket = &sendPacketState{} + } + state := p.sendPacket + state.mu.Lock() + p.sendPacketMu.Unlock() + defer state.mu.Unlock() + + fnAddr, err := state.ensureFunction(p) + if err != nil { + return fmt.Errorf("resolve D2GS_SendPacket: %w", err) + } + + handle, err := state.ensureHandle(p.pid) + if err != nil { + return fmt.Errorf("open process: %w", err) + } + + if err := state.ensureStub(handle); err != nil { + return err + } + + if err := state.ensureMeta(handle); err != nil { + return err + } + + if err := state.ensurePacketBuffer(handle, uintptr(len(packet))); err != nil { + return err + } + + if err := writeRemoteMemory(handle, state.packet, packet); err != nil { + return fmt.Errorf("write remote packet: %w", err) + } + + metaBuf := metaBufPool.Get().(*[sendPacketMetaSize]byte) + defer func() { + *metaBuf = [sendPacketMetaSize]byte{} + metaBufPool.Put(metaBuf) + }() + + binary.LittleEndian.PutUint64(metaBuf[0:], uint64(fnAddr)) + binary.LittleEndian.PutUint64(metaBuf[8:], uint64(state.packet)) + binary.LittleEndian.PutUint64(metaBuf[16:], uint64(len(packet))) + binary.LittleEndian.PutUint32(metaBuf[sendPacketStatusOffset:], 0) + + if err := writeRemoteMemory(handle, state.meta, metaBuf[:]); err != nil { + return fmt.Errorf("write remote metadata: %w", err) + } + + threadHandle, err := state.ensureThreadHandle(p) + if err != nil { + return fmt.Errorf("resolve main thread: %w", err) + } + + if err := state.dispatchAPC(threadHandle, state.stub, state.meta); err != nil { + return fmt.Errorf("dispatch APC: %w", err) + } + + return nil +} + +func writeRemoteMemory(handle windows.Handle, address uintptr, data []byte) error { + if address == 0 { + return errors.New("attempt to write to null address") + } + if len(data) == 0 { + return nil + } + return windows.WriteProcessMemory(handle, address, &data[0], uintptr(len(data)), nil) +} + +func virtualAllocEx(handle windows.Handle, size uintptr, protect uint32) (uintptr, error) { + if size == 0 { + return 0, errors.New("allocation size cannot be zero") + } + + 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 { + if address == 0 { + 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 +}