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 }