v0.1 - initial commit
This commit is contained in:
@@ -0,0 +1,238 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user