Files
2026-07-18 21:37:15 +03:00

172 lines
4.9 KiB
Go

package winpwn
import (
"bytes"
"debug/pe"
"errors"
"io"
"math"
)
// Section characteristic flags relevant to memory protection
// (IMAGE_SCN_MEM_*, see winnt.h).
const (
imageSCNMemExecute = 0x20000000
imageSCNMemRead = 0x40000000
imageSCNMemWrite = 0x80000000
)
// Section wraps debug/pe.Section with the permission/entropy helpers a pwn
// workflow actually needs (find the RWX section, spot the packed one).
type Section struct {
*pe.Section
// live is set when this Section belongs to a live-process-backed
// PEFile. debug/pe.Section.Data() always reads via PointerToRawData
// (the section's *file* offset), which is wrong once the image is
// loaded into memory -- SectionAlignment shifts things around relative
// to FileAlignment. When live is set, Data() reads VirtualSize bytes
// at VirtualAddress instead, through the same ReaderAt the rest of a
// live PEFile uses.
live io.ReaderAt
}
func (s *Section) IsReadable() bool { return s.Characteristics&imageSCNMemRead != 0 }
func (s *Section) IsWritable() bool { return s.Characteristics&imageSCNMemWrite != 0 }
func (s *Section) IsExecutable() bool { return s.Characteristics&imageSCNMemExecute != 0 }
func (s *Section) IsRWX() bool { return s.IsReadable() && s.IsWritable() && s.IsExecutable() }
// Data returns the section's raw bytes. Overrides (shadows)
// debug/pe.Section.Data: see the live field's doc comment for why a
// live-process-backed section needs a different read path.
func (s *Section) Data() ([]byte, error) {
if s.live == nil {
return s.Section.Data()
}
buf := make([]byte, s.VirtualSize)
if _, err := s.live.ReadAt(buf, int64(s.VirtualAddress)); err != nil {
return nil, err
}
return buf, nil
}
// Entropy returns the Shannon entropy (0..8 bits/byte) of the section's raw
// data, the standard quick signal for "this is packed/encrypted" (UPX-style
// sections commonly read >7.2).
func (s *Section) Entropy() (float64, error) {
data, err := s.Data()
if err != nil {
return 0, err
}
return ShannonEntropy(data), nil
}
// ShannonEntropy computes the byte-level Shannon entropy of data, in bits
// per byte (0 = uniform/empty, 8 = maximally random).
func ShannonEntropy(data []byte) float64 {
if len(data) == 0 {
return 0
}
var counts [256]int
for _, b := range data {
counts[b]++
}
entropy := 0.0
total := float64(len(data))
for _, c := range counts {
if c == 0 {
continue
}
freq := float64(c) / total
entropy -= freq * math.Log2(freq)
}
return entropy
}
// Sections returns every section wrapped with the permission/entropy helpers.
func (p *PEFile) Sections() []*Section {
out := make([]*Section, len(p.File.Sections))
for i, sec := range p.File.Sections {
out[i] = p.wrapSection(sec)
}
return out
}
// Section looks up a single section by name (e.g. ".text").
func (p *PEFile) Section(name string) (*Section, error) {
for _, sec := range p.File.Sections {
if sec.Name == name {
return p.wrapSection(sec), nil
}
}
return nil, errors.New("section not found: " + name)
}
func (p *PEFile) wrapSection(sec *pe.Section) *Section {
s := &Section{Section: sec}
if p.live {
s.live = p.r
}
return s
}
// LikelyPackedSections returns sections whose entropy exceeds threshold
// (0 selects the common UPX-style default of 7.2 bits/byte), the quick
// "is this binary packed" check pwntools has no direct analogue for since
// ELF packers are rarer in CTF practice than UPX-on-Windows.
func (p *PEFile) LikelyPackedSections(threshold float64) ([]*Section, error) {
if threshold <= 0 {
threshold = 7.2
}
var hits []*Section
for _, sec := range p.Sections() {
if sec.Size == 0 {
continue
}
entropy, err := sec.Entropy()
if err != nil {
continue
}
if entropy >= threshold {
hits = append(hits, sec)
}
}
return hits, nil
}
// SearchBytes ищет последовательность байт во всех секциях PE-файла
// Возвращает массив RVA (Relative Virtual Address) всех совпадений
func (p *PEFile) SearchBytes(pattern []byte) ([]uint64, error) {
var results []uint64
for _, sec := range p.Sections() {
// Читаем сырые данные секции (live-aware: see Section.Data)
data, err := sec.Data()
if err != nil {
continue // Если секция пустая (например .bss), пропускаем
}
offset := 0
for {
// Ищем паттерн в оставшейся части данных
idx := bytes.Index(data[offset:], pattern)
if idx == -1 {
break
}
// Вычисляем RVA: виртуальный адрес секции + смещение внутри секции
rva := sec.VirtualAddress + uint32(offset+idx)
results = append(results, uint64(rva))
// Сдвигаем offset, чтобы продолжить поиск после текущего совпадения
offset += idx + 1
}
}
if len(results) == 0 {
return nil, errors.New("pattern not found in PE file")
}
return results, nil
}