172 lines
4.9 KiB
Go
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
|
|
}
|