239 lines
7.3 KiB
Go
239 lines
7.3 KiB
Go
package winpwn
|
|
|
|
import (
|
|
"debug/pe"
|
|
"encoding/binary"
|
|
"errors"
|
|
"os"
|
|
"unsafe"
|
|
)
|
|
|
|
// OpenPEForWrite opens a PE file read-write, for the patching methods below.
|
|
// Plain OpenPE is read-only by design; writing is opt-in so a script can't
|
|
// accidentally corrupt a target binary it only meant to inspect.
|
|
func OpenPEForWrite(path string) (*PEFile, error) {
|
|
fd, err := os.OpenFile(path, os.O_RDWR, 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
f, err := pe.NewFile(fd)
|
|
if err != nil {
|
|
fd.Close()
|
|
return nil, err
|
|
}
|
|
info, err := fd.Stat()
|
|
if err != nil {
|
|
fd.Close()
|
|
return nil, err
|
|
}
|
|
return &PEFile{File: f, r: fd, w: fd, closer: fd, size: info.Size()}, nil
|
|
}
|
|
|
|
// PatchBytes overwrites the file's contents at the given RVA with data, the
|
|
// general-purpose "patch on the fly" primitive.
|
|
func (p *PEFile) PatchBytes(rva uint32, data []byte) error {
|
|
offset := p.RVAToFileOffset(rva)
|
|
if offset == 0 {
|
|
return errors.New("RVA does not map to any section")
|
|
}
|
|
return p.PatchBytesAtOffset(offset, data)
|
|
}
|
|
|
|
// PatchBytesAtOffset overwrites the file's contents at a raw file offset.
|
|
func (p *PEFile) PatchBytesAtOffset(offset int64, data []byte) error {
|
|
return p.writeAt(offset, data)
|
|
}
|
|
|
|
// peHeaderOffset reads e_lfanew (at the fixed DOS-header offset 0x3C) to
|
|
// find where the "PE\0\0" header begins.
|
|
func (p *PEFile) peHeaderOffset() (int64, error) {
|
|
var lfanew uint32
|
|
if err := p.readStructAt(0x3C, &lfanew); err != nil {
|
|
return 0, err
|
|
}
|
|
return int64(lfanew), nil
|
|
}
|
|
|
|
// coffHeaderOffset returns the file offset of the COFF File Header, right
|
|
// after the 4-byte "PE\0\0" signature.
|
|
func (p *PEFile) coffHeaderOffset() (int64, error) {
|
|
peOffset, err := p.peHeaderOffset()
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return peOffset + 4, nil
|
|
}
|
|
|
|
// optionalHeaderOffset returns the file offset of the Optional Header,
|
|
// right after the fixed 20-byte COFF File Header.
|
|
func (p *PEFile) optionalHeaderOffset() (int64, error) {
|
|
coffOffset, err := p.coffHeaderOffset()
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return coffOffset + 20, nil
|
|
}
|
|
|
|
// sectionHeaderTableOffset returns the file offset of the first
|
|
// IMAGE_SECTION_HEADER entry, right after the Optional Header.
|
|
func (p *PEFile) sectionHeaderTableOffset() (int64, error) {
|
|
optOffset, err := p.optionalHeaderOffset()
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return optOffset + int64(p.File.FileHeader.SizeOfOptionalHeader), nil
|
|
}
|
|
|
|
// imageSectionHeaderSize and the byte offset of the Characteristics field
|
|
// within it (IMAGE_SECTION_HEADER: Name[8] + 6 DWORDs + 2 WORDs + Characteristics DWORD).
|
|
const (
|
|
imageSectionHeaderSize = 40
|
|
imageSectionHeaderCharacteristicsOff = 36
|
|
)
|
|
|
|
// SetSectionCharacteristics overwrites a section's Characteristics flags
|
|
// directly in the section header — e.g. to flip on IMAGE_SCN_MEM_EXECUTE for
|
|
// a section you want to use as shellcode landing space. Requires a PEFile
|
|
// opened with OpenPEForWrite.
|
|
func (p *PEFile) SetSectionCharacteristics(name string, characteristics uint32) error {
|
|
tableOffset, err := p.sectionHeaderTableOffset()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for i, sec := range p.File.Sections {
|
|
if sec.Name != name {
|
|
continue
|
|
}
|
|
headerOffset := tableOffset + int64(i)*imageSectionHeaderSize
|
|
var buf [4]byte
|
|
binary.LittleEndian.PutUint32(buf[:], characteristics)
|
|
return p.PatchBytesAtOffset(headerOffset+imageSectionHeaderCharacteristicsOff, buf[:])
|
|
}
|
|
return errors.New("section not found: " + name)
|
|
}
|
|
|
|
// MakeSectionExecutable ORs in IMAGE_SCN_MEM_EXECUTE on top of a section's
|
|
// existing characteristics (e.g. "make .data executable" for a quick and
|
|
// dirty shellcode-in-data-section trick).
|
|
func (p *PEFile) MakeSectionExecutable(name string) error {
|
|
return p.orSectionCharacteristics(name, imageSCNMemExecute)
|
|
}
|
|
|
|
// MakeSectionWritable ORs in IMAGE_SCN_MEM_WRITE on top of a section's
|
|
// existing characteristics.
|
|
func (p *PEFile) MakeSectionWritable(name string) error {
|
|
return p.orSectionCharacteristics(name, imageSCNMemWrite)
|
|
}
|
|
|
|
func (p *PEFile) orSectionCharacteristics(name string, flag uint32) error {
|
|
for _, sec := range p.File.Sections {
|
|
if sec.Name == name {
|
|
return p.SetSectionCharacteristics(name, sec.Characteristics|flag)
|
|
}
|
|
}
|
|
return errors.New("section not found: " + name)
|
|
}
|
|
|
|
// imageTLSDirectory mirrors winnt.h's IMAGE_TLS_DIRECTORY32/64: same field
|
|
// order in both, only pointer-sized members change width. All fields here
|
|
// are absolute VAs, not RVAs — the one PE directory that isn't RVA-based.
|
|
type imageTLSDirectory64 struct {
|
|
StartAddressOfRawData uint64
|
|
EndAddressOfRawData uint64
|
|
AddressOfIndex uint64
|
|
AddressOfCallBacks uint64
|
|
}
|
|
|
|
type imageTLSDirectory32 struct {
|
|
StartAddressOfRawData uint32
|
|
EndAddressOfRawData uint32
|
|
AddressOfIndex uint32
|
|
AddressOfCallBacks uint32
|
|
}
|
|
|
|
// DisableTLSCallbacks zeroes the AddressOfCallBacks field of the TLS
|
|
// Directory, so the loader never walks (and never invokes) the callback
|
|
// array at all — the one-field patch that defeats TLS-callback-based
|
|
// anti-debug/anti-instrumentation tricks that fire before your entry point
|
|
// or your debugger's first breakpoint gets a chance to run.
|
|
func (p *PEFile) DisableTLSCallbacks() error {
|
|
h, err := p.header()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
const dirEntryTLS = 9
|
|
dir := h.dataDirectory[dirEntryTLS]
|
|
if dir.VirtualAddress == 0 {
|
|
return errors.New("no TLS directory present")
|
|
}
|
|
offset := p.RVAToFileOffset(dir.VirtualAddress)
|
|
if offset == 0 {
|
|
return errors.New("failed to map TLS directory RVA to file offset")
|
|
}
|
|
|
|
if h.is64 {
|
|
zeros := make([]byte, 8)
|
|
return p.PatchBytesAtOffset(offset+int64(unsafe.Offsetof(imageTLSDirectory64{}.AddressOfCallBacks)), zeros)
|
|
}
|
|
zeros := make([]byte, 4)
|
|
return p.PatchBytesAtOffset(offset+int64(unsafe.Offsetof(imageTLSDirectory32{}.AddressOfCallBacks)), zeros)
|
|
}
|
|
|
|
// RecalculateChecksum recomputes and writes the Optional Header's PE
|
|
// checksum (the algorithm behind imagehlp's CheckSumMappedFile/MapFileAndCheckSum),
|
|
// so a binary you've patched on disk still passes loaders/AV/signing tools
|
|
// that validate it.
|
|
func (p *PEFile) RecalculateChecksum() error {
|
|
h, err := p.header()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
optOffset, err := p.optionalHeaderOffset()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
checksumOffset := optOffset
|
|
if h.is64 {
|
|
checksumOffset += int64(unsafe.Offsetof(pe.OptionalHeader64{}.CheckSum))
|
|
} else {
|
|
checksumOffset += int64(unsafe.Offsetof(pe.OptionalHeader32{}.CheckSum))
|
|
}
|
|
|
|
data := make([]byte, p.size)
|
|
if _, err := p.r.ReadAt(data, 0); err != nil {
|
|
return err
|
|
}
|
|
|
|
checksum := peChecksum(data, checksumOffset)
|
|
|
|
var buf [4]byte
|
|
binary.LittleEndian.PutUint32(buf[:], checksum)
|
|
return p.PatchBytesAtOffset(checksumOffset, buf[:])
|
|
}
|
|
|
|
// peChecksum implements the PE checksum algorithm: sum the file as 16-bit
|
|
// little-endian words (treating the existing 4-byte checksum field as if it
|
|
// contributed zero), fold carries back into the low 16 bits, then add the
|
|
// file size.
|
|
func peChecksum(data []byte, checksumFieldOffset int64) uint32 {
|
|
var checksum uint32
|
|
|
|
n := len(data)
|
|
for i := 0; i < n; i += 2 {
|
|
if int64(i) == checksumFieldOffset || int64(i) == checksumFieldOffset+2 {
|
|
continue // skip the checksum field's own two words
|
|
}
|
|
var word uint32
|
|
if i+1 < n {
|
|
word = uint32(data[i]) | uint32(data[i+1])<<8
|
|
} else {
|
|
word = uint32(data[i]) // trailing odd byte
|
|
}
|
|
checksum = (checksum & 0xFFFF) + word + (checksum >> 16)
|
|
}
|
|
checksum = (checksum & 0xFFFF) + (checksum >> 16)
|
|
checksum += uint32(n)
|
|
return checksum
|
|
}
|