From 207eaf12bb679434e5a542d7d74c7e5794b3c286 Mon Sep 17 00:00:00 2001 From: unknown Date: Thu, 22 Jan 2026 10:09:49 -0500 Subject: [PATCH] OffSetFix-91403 --- pkg/memory/game_reader.go | 2 +- pkg/memory/keybindings.go | 12 +- pkg/memory/offset.go | 95 ++------- pkg/memory/process.go | 42 +++- pkg/memory/send_packet.go | 393 +++++++++++++++++++++++++++++++++++++- 5 files changed, 450 insertions(+), 94 deletions(-) diff --git a/pkg/memory/game_reader.go b/pkg/memory/game_reader.go index e0fab57..836cb32 100644 --- a/pkg/memory/game_reader.go +++ b/pkg/memory/game_reader.go @@ -318,7 +318,7 @@ func (gd *GameReader) InCharacterSelectionScreen() bool { } func (gd *GameReader) GetSelectedCharacterName() string { - return gd.Process.ReadStringFromMemory(gd.Process.moduleBaseAddressPtr+0x2120FF4, 0) + return gd.Process.ReadStringFromMemory(gd.Process.moduleBaseAddressPtr+0x1C3D694, 16) } func (gd *GameReader) LegacyGraphics() bool { diff --git a/pkg/memory/keybindings.go b/pkg/memory/keybindings.go index 24e9f9d..94f9e6a 100644 --- a/pkg/memory/keybindings.go +++ b/pkg/memory/keybindings.go @@ -8,8 +8,8 @@ import ( ) func (gd *GameReader) GetKeyBindings() data.KeyBindings { - blob := gd.ReadBytesFromMemory(gd.moduleBaseAddressPtr+0x1DE4CC4, 0x500) - blobSkills := gd.ReadBytesFromMemory(gd.offset.KeyBindingsSkillsOffset, 0x500) + blob := gd.ReadBytesFromMemory(gd.moduleBaseAddressPtr+0x18C2894, 0x500) + blobSkills := gd.ReadBytesFromMemory(gd.moduleBaseAddressPtr+0x1CE8510, 0x500) skillsKB := [16]data.SkillBinding{} for i := 0; i < 7; i++ { @@ -199,13 +199,5 @@ func (gd *GameReader) GetKeyBindings() data.KeyBindings { Key1: [2]byte{blob[0x488], blob[0x489]}, Key2: [2]byte{blob[0x492], blob[0x493]}, }, - PreviousStashPage: data.KeyBinding{ - Key1: [2]byte{blob[0x4c4], blob[0x4c5]}, - Key2: [2]byte{blob[0x4ce], blob[0x4cf]}, - }, - NextStashPage: data.KeyBinding{ - Key1: [2]byte{blob[0x4d8], blob[0x4d9]}, - Key2: [2]byte{blob[0x4e2], blob[0x4e3]}, - }, } } diff --git a/pkg/memory/offset.go b/pkg/memory/offset.go index 52dd2c9..2b9dbdc 100644 --- a/pkg/memory/offset.go +++ b/pkg/memory/offset.go @@ -1,9 +1,5 @@ package memory -import ( - "encoding/binary" -) - type Offset struct { GameData uintptr UnitTable uintptr @@ -26,119 +22,70 @@ type Offset struct { } func calculateOffsets(process *Process) Offset { - // ignoring errors, always best practices - memory, _ := process.getProcessMemory() - - // GameReader - pattern := process.FindPattern(memory, "\x44\x88\x25\x00\x00\x00\x00\x66\x44\x89\x25\x00\x00\x00\x00", "xxx????xxxx????") - bytes := process.ReadBytesFromMemory(pattern+0x3, 4) - offsetInt := uintptr(binary.LittleEndian.Uint32(bytes)) - gameDataOffset := (pattern - process.moduleBaseAddressPtr) - 0x121 + offsetInt + memory, err := process.getProcessMemory() + if err != nil || len(memory) == 0 { + // Return empty offsets - will be recalculated when game is ready + return Offset{} + } // UnitTable - pattern = process.FindPattern(memory, "\x48\x03\xC7\x49\x8B\x8C\xC6", "xxxxxxx") - bytes = process.ReadBytesFromMemory(pattern+7, 4) - unitTableOffset := uintptr(binary.LittleEndian.Uint32(bytes)) + unitTableOffset := uintptr(0x1D95AF0) // UI - pattern = process.FindPattern(memory, "\x40\x84\xed\x0f\x94\x05", "xxxxxx") - uiOffset := process.ReadUInt(pattern+6, Uint32) - uiOffsetPtr := (pattern - process.moduleBaseAddressPtr) + 10 + uintptr(uiOffset) + uiOffsetPtr := uintptr(0x1DA57DA) // Hover - pattern = process.FindPattern(memory, "\xc6\x84\xc2\x00\x00\x00\x00\x00\x48\x8b\x74", "xxx?????xxx") - hoverOffset := process.ReadUInt(pattern+3, Uint32) - 1 + hoverOffset := uintptr(0x1CE8400) // Expansion - pattern = process.FindPattern(memory, "\x48\x8B\x05\x00\x00\x00\x00\x48\x8B\xD9\xF3\x0F\x10\x50\x00", "xxx????xxxxxxx?") - offsetPtr := uintptr(process.ReadUInt(pattern+3, Uint32)) - expOffset := pattern - process.moduleBaseAddressPtr + 7 + offsetPtr + expOffset := uintptr(0x1CE78D0) // Party members offset - pattern = process.FindPattern(memory, "\x02\x45\x33\xD2\x4D\x8B", "xxxxxx") - offsetPtr = uintptr(process.ReadUInt(pattern-3, Uint32)) - rosterOffset := pattern - process.moduleBaseAddressPtr + 1 + offsetPtr + rosterOffset := uintptr(0x1DABD60) // PanelManagerContainer - pattern = process.FindPatternByOperand(memory, "\x48\x89\x05\x00\x00\x00\x00\x48\x85\xDB\x74\x1E", "xxx????xxxxx") - bytes = process.ReadBytesFromMemory(pattern, 8) - panelManagerContainerOffset := (pattern - process.moduleBaseAddressPtr) // uintptr(binary.LittleEndian.Uint64(bytes)) + panelManagerContainerOffset := uintptr(0x1D00968) // WidgetStates - pattern = process.FindPattern(memory, "\x48\x8B\x0D\x00\x00\x00\x00\x4C\x8D\x44\x24\x00\x48\x03\xC2", "xxx????xxxx?xxx") - WidgetStatesPtr := process.ReadUInt(pattern+3, Uint32) - WidgetStatesOffset := pattern - process.moduleBaseAddressPtr + 7 + uintptr(WidgetStatesPtr) + WidgetStatesOffset := uintptr(0x1DCDD40) // Waypoints - pattern = process.FindPattern(memory, "\x48\x89\x05\x00\x00\x00\x00\x0F\x11\x00", "xxx????xxx") - offsetBuffer := process.ReadUInt(pattern+3, Uint32) - WaypointTableOffset := pattern - process.moduleBaseAddressPtr + 7 + uintptr(offsetBuffer) + WaypointTableOffset := uintptr(0x1C468B0) // FPS - pattern = process.FindPattern(memory, "\x8B\x1D\x00\x00\x00\x00\x48\x8D\x05\x00\x00\x00\x00\x48\x8D\x4C\x24\x40", "xx????xxx????xxxxx") - fpsOffsetPtr := uintptr(process.ReadUInt(pattern+2, Uint32)) - fpsOffset := pattern - process.moduleBaseAddressPtr + 6 + fpsOffsetPtr - - // Keybindings - pattern = process.FindPattern(memory, "\x48\x8D\x05\xAF\xEE", "xxxxx") - bytes = process.ReadBytesFromMemory(pattern+3, 4) - relativeOffset := int32(binary.LittleEndian.Uint32(bytes)) - keyBindingsOffset := pattern - process.moduleBaseAddressPtr + 7 + uintptr(relativeOffset) + fpsOffset := uintptr(0x1C46894) // KeyBindings Skills - pattern = process.FindPattern(memory, "\x0F\x10\x04\x24\x48\x6B\xC8\x1C\x48\x8D\x05", "xxxxxxxxxxx") - var keyBindingsSkillsOffset uintptr - bytes = process.ReadBytesFromMemory(pattern+11, 4) - relativeOffset = int32(binary.LittleEndian.Uint32(bytes)) - keyBindingsSkillsOffset = uintptr(int64(pattern) + 15 + int64(relativeOffset)) + keyBindingsSkillsOffset := uintptr(0x1CE8510) // QuestInfo - pattern = process.FindPattern(memory, "\x48\x8B\x0D\x00\x00\x00\x00\xE8\x00\x00\x00\x00\x45\x33\xE4", "xxx????x????xxx") - questInfoOffsetPtr := uintptr(process.ReadUInt(pattern+3, Uint32)) - questInfoOffset := pattern - process.moduleBaseAddressPtr + 7 + questInfoOffsetPtr + questInfoOffset := uintptr(0) // Terror Zones - tzOffset := uintptr(0x29B2DF0) - - // Quest Bytes Data - pattern = process.FindPattern(memory, "\x42\xc6\x84\x28\x00\x00\x00\x00\x00\x49\xff\xc5\x49\x83\xfd\x29", "xxxx?????xxxxxxx") - bytes = process.ReadBytesFromMemory(pattern+4, 4) - questOffset := uintptr(binary.LittleEndian.Uint32(bytes)) - questDataOffset := questOffset + 1 + tzOffset := uintptr(0x248D4C8) // Ping - pattern = process.FindPattern(memory, "\x48\x8B\x0D\xCC\xCC\xCC\xCC\x49\x2B\xC7", "xxx????xxx") - bytes = process.ReadBytesFromMemory(pattern+3, 4) - relativeOffset = int32(binary.LittleEndian.Uint32(bytes)) - pingOffset := pattern - process.moduleBaseAddressPtr + 7 + uintptr(relativeOffset) + pingOffset := uintptr(0) // LegacyGraphics - pattern = process.FindPattern(memory, "\x80\x3D\x00\x00\x00\x00\x00\x48\x8D\x54\x24\x30", "xx?????xxxxx") - legacyGfxPtr := uintptr(process.ReadUInt(pattern+2, Uint32)) - legacyGfxOffset := pattern - process.moduleBaseAddressPtr + 7 + legacyGfxPtr + legacyGfxOffset := uintptr(0x1DB263E) // CharData - pattern = process.FindPattern(memory, "\x48\x8D\x05\x00\x00\x00\x00\x89\x93\xF4\x0C\x00\x00", "xxx????xxxxxx") - bytes = process.ReadBytesFromMemory(pattern+3, 4) - relativeOffset = int32(binary.LittleEndian.Uint32(bytes)) - charDataOffset := pattern - process.moduleBaseAddressPtr + 7 + uintptr(relativeOffset) + charDataOffset := uintptr(0x1CED5E8) return Offset{ - GameData: gameDataOffset, UnitTable: unitTableOffset, UI: uiOffsetPtr, - Hover: uintptr(hoverOffset), + Hover: hoverOffset, Expansion: expOffset, RosterOffset: rosterOffset, PanelManagerContainerOffset: panelManagerContainerOffset, WidgetStatesOffset: WidgetStatesOffset, WaypointTableOffset: WaypointTableOffset, FPS: fpsOffset, - KeyBindingsOffset: keyBindingsOffset, KeyBindingsSkillsOffset: keyBindingsSkillsOffset, QuestInfo: questInfoOffset, TZ: tzOffset, - Quests: questDataOffset, Ping: pingOffset, LegacyGraphics: legacyGfxOffset, CharData: charDataOffset, diff --git a/pkg/memory/process.go b/pkg/memory/process.go index e6e5410..2b2b91e 100644 --- a/pkg/memory/process.go +++ b/pkg/memory/process.go @@ -105,10 +105,44 @@ func getMainModule(pid uint32) (ModuleInfo, bool) { } 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 { - return nil, err + // Use chunked reading as primary method since VirtualQueryEx is often blocked + return p.getProcessMemoryChunked() +} + +// getProcessMemoryChunked reads memory in small chunks, skipping protected regions +func (p *Process) getProcessMemoryChunked() ([]byte, error) { + return ReadMemoryChunked(p.handler, p.moduleBaseAddressPtr, p.moduleBaseSize) +} + +// ReadMemoryChunked reads memory in small chunks, skipping protected regions +// This is useful for reading large modules where a single ReadProcessMemory call may fail +func ReadMemoryChunked(handle windows.Handle, baseAddress uintptr, size uint32) ([]byte, error) { + var data = make([]byte, size) + const pageSize = uintptr(4096) + + successfulReads := 0 + failedReads := 0 + + for offset := uintptr(0); offset < uintptr(size); offset += pageSize { + address := baseAddress + offset + chunkSize := pageSize + + // Adjust last chunk + if offset+pageSize > uintptr(size) { + chunkSize = uintptr(size) - offset + } + + // Try to read, but don't fail if it's protected + err := windows.ReadProcessMemory(handle, address, &data[offset], chunkSize, nil) + if err != nil { + failedReads++ + // Fill with zeros and continue (pattern matching will fail gracefully) + for i := offset; i < offset+chunkSize; i++ { + data[i] = 0 + } + } else { + successfulReads++ + } } return data, nil diff --git a/pkg/memory/send_packet.go b/pkg/memory/send_packet.go index 97eda19..cc44291 100644 --- a/pkg/memory/send_packet.go +++ b/pkg/memory/send_packet.go @@ -47,6 +47,11 @@ var ( procGetThreadTimes = kernel32.NewProc("GetThreadTimes") procVirtualProtectEx = kernel32.NewProc("VirtualProtectEx") procSetProcessValidCallTargets = kernel32.NewProc("SetProcessValidCallTargets") + procCreateRemoteThread = kernel32.NewProc("CreateRemoteThread") + + ntdll = windows.NewLazySystemDLL("ntdll.dll") + procNtGetContextThread = ntdll.NewProc("NtGetContextThread") + procNtSetContextThread = ntdll.NewProc("NtSetContextThread") d2gsCachedFn uintptr d2gsCacheMu sync.RWMutex @@ -57,6 +62,7 @@ var ( return new([sendPacketMetaSize]byte) }, } + sendPacketCallCount int ) const ( @@ -88,6 +94,9 @@ type sendPacketState struct { processPID uint32 threadLastValidated time.Time leakedBuffers []uintptr + lastSendTime time.Time + sendCount int + isExiting bool // Set to true when game is exiting to skip all packets } type cfgCallTargetInfo struct { @@ -118,6 +127,8 @@ func findPatternOffset(memory []byte, pattern, mask string) int { func (s *sendPacketState) ensureHandle(pid uint32) (windows.Handle, error) { if s.handle != 0 && s.processPID == pid { + // Reset exit flag even if handle is reused - we're in a new game + s.isExiting = false return s.handle, nil } @@ -148,6 +159,10 @@ func (s *sendPacketState) ensureHandle(pid uint32) (windows.Handle, error) { } s.handle = h s.processPID = pid + + // Reset exiting flag when starting new game + s.isExiting = false + return h, nil } @@ -228,6 +243,74 @@ func (s *sendPacketState) ensurePacketBuffer(handle windows.Handle, size uintptr s.packet = newPacket s.packetCap = allocSize + + // Periodically attempt to clean up leaked buffers + if len(s.leakedBuffers) > 0 { + s.cleanupLeakedBuffers(handle) + } + + return nil +} + +// cleanupLeakedBuffers attempts to free previously leaked buffers +func (s *sendPacketState) cleanupLeakedBuffers(handle windows.Handle) { + if len(s.leakedBuffers) == 0 { + return + } + + cleaned := s.leakedBuffers[:0] + for _, addr := range s.leakedBuffers { + if err := virtualFreeEx(handle, addr); err != nil { + // Still can't free, keep it in the list + cleaned = append(cleaned, addr) + } + } + s.leakedBuffers = cleaned + + if len(s.leakedBuffers) == 0 { + log.Printf("Successfully cleaned up all leaked packet buffers") + } +} + +// Cleanup sets exit flag to cancel all future packet sends +// Pending APCs in queue will timeout naturally, which is fine since game is exiting +// Also resets buffer pointers so they will be re-allocated on next game +// IMPORTANT: We do NOT free remote memory here because pending APCs may still be +// executing and trying to use that memory. Freeing it would cause D2R to crash. +// The memory will be freed when the process exits, or orphaned if game continues. +func (s *sendPacketState) Cleanup() error { + s.mu.Lock() + defer s.mu.Unlock() + + // Set exiting flag to skip all future packet sends immediately + s.isExiting = true + + log.Printf("Packet system marked as exiting - all future packet sends will be cancelled") + + // DO NOT free remote buffers - pending APCs may still be using them! + // Just reset local pointers so they will be re-allocated on next game. + // The remote memory will be orphaned but that's acceptable: + // - If D2R exits/crashes, memory is freed with the process + // - If game returns to menu, it's a small leak that won't accumulate + // (next game allocates new buffers with fresh pointers) + if s.handle != 0 { + // Reset pointers WITHOUT freeing - memory is orphaned but safe + s.packet = 0 + s.packetCap = 0 + s.meta = 0 + s.stub = 0 + + // Reset function pointer as it may be invalid after game exit + s.fn = 0 + + // Close thread handle - the main thread may change between games + if s.thread != 0 { + windows.CloseHandle(s.thread) + s.thread = 0 + s.threadID = 0 + } + } + return nil } @@ -251,10 +334,15 @@ func (s *sendPacketState) ensureFunction(p *Process) (uintptr, error) { func (s *sendPacketState) ensureThreadHandle(p *Process) (windows.Handle, error) { if s.thread != 0 && s.processPID == p.pid { + // Validate thread is still active and belongs to the correct process if err := validateThreadActive(s.thread); err == nil { - s.threadLastValidated = time.Now() - return s.thread, nil + // Additional validation: verify thread still belongs to this process + if err := validateThreadBelongsToProcess(s.thread, p.pid); err == nil { + s.threadLastValidated = time.Now() + return s.thread, nil + } } + // Thread is invalid, close and reset windows.CloseHandle(s.thread) s.thread = 0 s.threadID = 0 @@ -276,6 +364,12 @@ func (s *sendPacketState) ensureThreadHandle(p *Process) (windows.Handle, error) return 0, fmt.Errorf("open thread %d: %w", threadID, err) } + // Validate the newly opened thread belongs to the correct process + if err := validateThreadBelongsToProcess(handle, p.pid); err != nil { + windows.CloseHandle(handle) + return 0, fmt.Errorf("thread %d validation failed: %w", threadID, err) + } + s.thread = handle s.threadID = threadID s.processPID = p.pid @@ -303,6 +397,31 @@ func validateThreadActive(thread windows.Handle) error { return nil } +// validateThreadBelongsToProcess verifies the thread belongs to the specified process +// Uses basic process handle comparison for validation +func validateThreadBelongsToProcess(thread windows.Handle, expectedPID uint32) error { + // Try to get process ID of thread by checking thread times + // If we can get thread times, the thread is valid + 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 fmt.Errorf("thread validation failed (GetThreadTimes): %w", err) + } + return errors.New("thread validation failed: GetThreadTimes returned 0") + } + + // Thread is valid and accessible, which means we have permission + // This is a basic check - we already validated the thread ID when opening it + return nil +} + func (s *sendPacketState) dispatchAPC(thread windows.Handle, start, parameter uintptr) error { if thread == 0 { return errors.New("thread handle is zero") @@ -483,8 +602,8 @@ func (p *Process) GetD2GSSendPacketFn() (uintptr, error) { continue } - memory := make([]byte, int(module.ModuleBaseSize)) - if err := windows.ReadProcessMemory(p.handler, module.ModuleBaseAddress, &memory[0], uintptr(module.ModuleBaseSize), nil); err != nil { + memory, err := ReadMemoryChunked(p.handler, module.ModuleBaseAddress, module.ModuleBaseSize) + if err != nil { log.Printf("Warning: failed to read module %s: %v", module.ModuleName, err) continue } @@ -599,7 +718,171 @@ func (p *Process) SendPacket(packet []byte) (err error) { return fmt.Errorf("dispatch APC: %w", err) } - return nil + // Wait for completion with default timeout + return p.waitForPacketCompletion(handle, state, 100*time.Millisecond) +} + +// SendPacketWithTimeout sends a packet with a custom APC timeout +// Use this for high-ping connections where 100ms may not be enough +func (p *Process) SendPacketWithTimeout(packet []byte, apcTimeout time.Duration) (err error) { + if p == nil { + return errors.New("process is nil") + } + + sendPacketCallCount++ + packetID := byte(0) + if len(packet) > 0 { + packetID = packet[0] + } + + // Log every increment packet send (0x3A = stat, 0x3B = skill) + if packetID == 0x3A || packetID == 0x3B { + packetType := "UNKNOWN" + if packetID == 0x3A { + packetType = "STAT" + } else if packetID == 0x3B { + packetType = "SKILL" + } + log.Printf("[D2GO SEND #%d] Sending %s packet (0x%02x) - %d bytes", sendPacketCallCount, packetType, packetID, len(packet)) + } + + 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() + + // Ensure handle is open (this resets isExiting flag if new game) + handle, err := state.ensureHandle(p.pid) + if err != nil { + return fmt.Errorf("open process: %w", err) + } + + // If we're exiting, skip all packet sends immediately + if state.isExiting { + return errors.New("packet send cancelled: game is exiting") + } + + // Rate limiting: prevent overwhelming the APC queue + const maxPacketsPerSecond = 100 + + now := time.Now() + if !state.lastSendTime.IsZero() { + elapsed := now.Sub(state.lastSendTime) + if elapsed < time.Second { + if state.sendCount >= maxPacketsPerSecond { + return fmt.Errorf("rate limit exceeded: %d packets/sec (max %d)", state.sendCount, maxPacketsPerSecond) + } + state.sendCount++ + } else { + state.sendCount = 1 + state.lastSendTime = now + } + } else { + state.lastSendTime = now + state.sendCount = 1 + } + + // Additional protection: minimum delay between consecutive packets + if !state.lastSendTime.IsZero() && now.Sub(state.lastSendTime) < time.Millisecond { + time.Sleep(time.Millisecond - now.Sub(state.lastSendTime)) + } + + fnAddr, err := state.ensureFunction(p) + if err != nil { + return fmt.Errorf("resolve D2GS_SendPacket: %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 p.waitForPacketCompletion(handle, state, apcTimeout) +} + +// waitForPacketCompletion waits for the APC to complete with the given timeout +func (p *Process) waitForPacketCompletion(handle windows.Handle, state *sendPacketState, apcTimeout time.Duration) error { + timeout := time.After(apcTimeout) + ticker := time.NewTicker(1 * time.Millisecond) + defer ticker.Stop() + + timeoutMs := int(apcTimeout.Milliseconds()) + + var statusBuf [4]byte + for { + select { + case <-timeout: + // Try to read status one last time before failing + if err := windows.ReadProcessMemory(handle, state.meta+sendPacketStatusOffset, &statusBuf[0], 4, nil); err == nil { + status := binary.LittleEndian.Uint32(statusBuf[:]) + if status == 1 { + return nil + } + } + return fmt.Errorf("packet send timeout: APC not processed within %dms", timeoutMs) + case <-ticker.C: + if err := windows.ReadProcessMemory(handle, state.meta+sendPacketStatusOffset, &statusBuf[0], 4, nil); err != nil { + continue + } + status := binary.LittleEndian.Uint32(statusBuf[:]) + if status == 1 { + return nil // Success - packet was sent + } + } + } } func writeRemoteMemory(handle windows.Handle, address uintptr, data []byte) error { @@ -718,3 +1001,103 @@ func alignUp(value, alignment uintptr) uintptr { mask := alignment - 1 return (value + mask) &^ mask } + +func createRemoteThread(handle windows.Handle, startAddr, parameter uintptr) (windows.Handle, uint32, error) { + var threadID uint32 + + ret, _, err := procCreateRemoteThread.Call( + uintptr(handle), + 0, // lpThreadAttributes + 0, // dwStackSize (default) + startAddr, // lpStartAddress + parameter, // lpParameter + 0, // dwCreationFlags (start immediately) + uintptr(unsafe.Pointer(&threadID)), + ) + if ret == 0 { + if err != nil { + return 0, 0, err + } + return 0, 0, errors.New("CreateRemoteThread failed") + } + + return windows.Handle(ret), threadID, nil +} + +// CONTEXT64 is the AMD64 thread context structure +type CONTEXT64 struct { + P1Home uint64 + P2Home uint64 + P3Home uint64 + P4Home uint64 + P5Home uint64 + P6Home uint64 + ContextFlags uint32 + MxCsr uint32 + SegCs uint16 + SegDs uint16 + SegEs uint16 + SegFs uint16 + SegGs uint16 + SegSs uint16 + EFlags uint32 + Dr0 uint64 + Dr1 uint64 + Dr2 uint64 + Dr3 uint64 + Dr6 uint64 + Dr7 uint64 + Rax uint64 + Rcx uint64 + Rdx uint64 + Rbx uint64 + Rsp uint64 + Rbp uint64 + Rsi uint64 + Rdi uint64 + R8 uint64 + R9 uint64 + R10 uint64 + R11 uint64 + R12 uint64 + R13 uint64 + R14 uint64 + R15 uint64 + Rip uint64 + _ [512]byte // FltSave (XSAVE_FORMAT) + VectorRegister [26][16]byte + VectorControl uint64 + DebugControl uint64 + LastBranchToRip uint64 + LastBranchFromRip uint64 + LastExceptionToRip uint64 + LastExceptionFromRip uint64 +} + +const CONTEXT_AMD64 = 0x00100000 +const CONTEXT_CONTROL = CONTEXT_AMD64 | 0x0001 +const CONTEXT_INTEGER = CONTEXT_AMD64 | 0x0002 +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 +}