add send packet function

This commit is contained in:
guiyomu-dev
2025-10-04 18:47:39 +02:00
parent ead8e9fbe3
commit 8c72ab6d50
4 changed files with 661 additions and 25 deletions

View File

@@ -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,

View File

@@ -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()

View File

@@ -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)
}

633
pkg/memory/send_packet.go Normal file
View File

@@ -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
}