sbf: add ELF section parser

This commit is contained in:
Richard Patel 2022-09-02 02:18:17 +02:00
parent cc798726bc
commit fade5aa0b8
2 changed files with 245 additions and 50 deletions

View File

@ -1,6 +1,7 @@
package sbf
import (
"bufio"
"bytes"
"debug/elf"
"encoding/binary"
@ -8,18 +9,35 @@ import (
"io"
"math"
"math/bits"
"strings"
)
// TODO Fuzz
// TODO Differential fuzz against rbpf
type Executable struct {
Header elf.Header64
Load elf.Prog64
ShStr elf.Section64
Header elf.Header64
Load elf.Prog64
ShShstrtab elf.Section64
ShSymtab *elf.Section64
ShStrtab *elf.Section64
ShDynstr *elf.Section64
}
// Bounds checks
const (
// 64 MiB max program size.
// Allows loader to use unchecked math when adding 32-bit offsets.
maxFileLen = 1 << 26
maxSectionNameLen = 16
maxSymbolNameLen = 1024
)
func LoadProgram(buf []byte) (*Executable, error) {
if len(buf) > maxFileLen {
return nil, fmt.Errorf("ELF file too large")
}
l := loader{
rd: bytes.NewReader(buf),
fileSize: uint64(len(buf)),
@ -27,6 +45,8 @@ func LoadProgram(buf []byte) (*Executable, error) {
return l.load()
}
const EF_SBF_V2 = 0x20
type loader struct {
rd io.ReaderAt
fileSize uint64
@ -44,11 +64,12 @@ func (l *loader) load() (*Executable, error) {
if err := l.loadProgramHeaderTable(); err != nil {
return nil, err
}
if err := l.validateSectionHeaderTable(); err != nil {
if err := l.readSectionHeaderTable(); err != nil {
return nil, err
}
if err := l.parseSections(); err != nil {
return nil, err
}
// TODO load section name section header
// TODO parse sections
// TODO parse dynamic segment
return l.elf, nil
}
@ -59,6 +80,16 @@ const (
shentsize = 0x40
)
func (l *loader) newPhTableIter() *tableIter[elf.Prog64] {
eh := &l.elf.Header
return newTableIterator[elf.Prog64](l, eh.Phoff, eh.Phnum, phentsize)
}
func (l *loader) newShTableIter() *tableIter[elf.Section64] {
eh := &l.elf.Header
return newTableIterator[elf.Section64](l, eh.Shoff, eh.Shnum, shentsize)
}
func (l *loader) readHeader() error {
var hdrBuf [ehsize]byte
if _, err := io.ReadFull(io.NewSectionReader(l.rd, 0, ehsize), hdrBuf[:]); err != nil {
@ -78,14 +109,14 @@ func (l *loader) validateHeader() error {
if elf.Class(ident[elf.EI_CLASS]) != elf.ELFCLASS64 ||
elf.Data(ident[elf.EI_DATA]) != elf.ELFDATA2LSB ||
elf.Version(ident[elf.EI_VERSION]) != elf.EV_CURRENT ||
elf.OSABI(ident[elf.EI_OSABI]) != elf.ELFOSABI_NONE {
elf.OSABI(ident[elf.EI_OSABI]) != elf.ELFOSABI_NONE ||
elf.Machine(eh.Machine) != elf.EM_BPF ||
elf.Type(eh.Type) != elf.ET_DYN {
return fmt.Errorf("incompatible binary")
}
// note: EI_PAD and EI_ABIVERSION are ignored
if elf.Machine(eh.Machine) != elf.EM_BPF ||
elf.Type(eh.Type) != elf.ET_DYN ||
eh.Version != uint32(elf.EV_CURRENT) ||
if eh.Version != uint32(elf.EV_CURRENT) ||
eh.Ehsize != ehsize ||
eh.Phentsize != phentsize ||
eh.Shentsize != shentsize ||
@ -108,20 +139,9 @@ func (l *loader) validateHeader() error {
// scan the program header table and remember the last PT_LOAD segment
func (l *loader) loadProgramHeaderTable() error {
eh := &l.elf.Header
phoff := eh.Phoff
for i := uint16(0); i < eh.Phnum; i++ {
if phoff+phentsize > math.MaxInt64 {
return io.ErrUnexpectedEOF
}
rd := io.NewSectionReader(l.rd, int64(phoff), phentsize)
var ph elf.Prog64
if err := binary.Read(rd, binary.LittleEndian, &ph); err != nil {
return err
}
phoff += phentsize
iter := l.newPhTableIter()
for iter.Next() && iter.Err() == nil {
ph := iter.Item()
if elf.ProgType(ph.Type) != elf.PT_LOAD {
continue
@ -139,37 +159,30 @@ func (l *loader) loadProgramHeaderTable() error {
l.elf.Load = ph
}
return nil
return iter.Err()
}
func (l *loader) validateSectionHeaderTable() error {
// reads and validates the section header table.
// remembers the section header table.
func (l *loader) readSectionHeaderTable() error {
eh := &l.elf.Header
shoff := eh.Shoff
iter := l.newShTableIter()
sectionDataOff := uint64(0)
offset := uint64(0)
for i := uint16(0); i < eh.Shnum; i++ {
if shoff+shentsize > math.MaxInt64 {
return io.ErrUnexpectedEOF
}
rd := io.NewSectionReader(l.rd, int64(shoff), shentsize)
if !iter.Next() {
return fmt.Errorf("missing section 0")
}
if elf.SectionType(iter.Item().Type) != elf.SHT_NULL {
return fmt.Errorf("section 0 is not SHT_NULL")
}
var sh elf.Section64
if err := binary.Read(rd, binary.LittleEndian, &sh); err != nil {
return err
}
shoff += shentsize
if i == 0 {
if elf.SectionType(sh.Type) != elf.SHT_NULL {
return fmt.Errorf("section 0 is not SHT_NULL")
}
continue
}
for iter.Next() && iter.Err() == nil {
i, sh := iter.Index(), iter.Item()
if elf.SectionType(sh.Type) == elf.SHT_NOBITS {
continue
}
// Ensure section data is not overlapping with ELF headers
shend, overflow := bits.Add64(sh.Off, sh.Size, 0)
if overflow != 0 {
return fmt.Errorf("integer overflow in section %d", i)
@ -183,16 +196,154 @@ func (l *loader) validateSectionHeaderTable() error {
if isOverlap(eh.Shoff, uint64(eh.Shnum)*shentsize, sh.Off, sh.Size) {
return fmt.Errorf("section %d overlaps with section header", i)
}
if eh.Shoff < offset {
// More checks
if eh.Shoff < sectionDataOff {
return fmt.Errorf("sections not in order")
}
if shend > l.fileSize {
return fmt.Errorf("section %d out of bounds", i)
}
offset = shend
// Remember section header string table.
if eh.Shstrndx != uint16(elf.SHN_UNDEF) && eh.Shstrndx == i {
l.elf.ShShstrtab = sh
}
sectionDataOff = shend
}
// TODO validate offset and size (?)
if elf.SectionType(l.elf.ShShstrtab.Type) != elf.SHT_STRTAB {
return fmt.Errorf("invalid .shstrtab")
}
return iter.Err()
}
func (l *loader) getString(strtab *elf.Section64, stroff uint32, maxLen uint16) (string, error) {
if elf.SectionType(strtab.Type) != elf.SHT_STRTAB {
return "", fmt.Errorf("invalid strtab")
}
offset := strtab.Off + uint64(stroff)
if offset > l.fileSize || offset+uint64(maxLen) > l.fileSize {
return "", io.ErrUnexpectedEOF
}
rd := bufio.NewReader(io.NewSectionReader(l.rd, int64(offset), int64(maxLen)))
var builder strings.Builder
for {
b, err := rd.ReadByte()
if err != nil {
return "", err
}
if b == 0 {
break
}
builder.WriteByte(b)
}
return builder.String(), nil
}
// Iterate sections and remember special sections by name.
func (l *loader) parseSections() error {
shShstrtab := &l.elf.ShShstrtab
iter := l.newShTableIter()
for iter.Next() && iter.Err() == nil {
sh := iter.Item()
sectionName, err := l.getString(shShstrtab, sh.Name, maxSectionNameLen)
if err != nil {
return fmt.Errorf("getString: %w", err)
}
// Remember special section or error if it already exists.
setSection := func(shPtr **elf.Section64) error {
if *shPtr != nil {
return fmt.Errorf("duplicate section: %s", sectionName)
}
*shPtr = new(elf.Section64)
**shPtr = sh
return nil
}
switch sectionName {
case ".symtab":
err = setSection(&l.elf.ShSymtab)
case ".strtab":
err = setSection(&l.elf.ShStrtab)
case ".dynstr":
err = setSection(&l.elf.ShDynstr)
}
if err != nil {
return err
}
}
return iter.Err()
}
// tableIter is a memory-efficient iterator over densely packed tables of statically sized items.
// Such as the ELF program header and section header tables.
type tableIter[T any] struct {
l *loader
off uint64
i uint16 // one ahead
count uint16
elemSize uint16
elem T
err error
}
// newTableIterator creates a new tableIter at `off` for `count` elements of `elemSize` len.
func newTableIterator[T any](l *loader, off uint64, count uint16, elemSize uint16) *tableIter[T] {
return &tableIter[T]{
l: l,
off: off,
count: count,
elemSize: elemSize,
}
}
// Next reads one element.
//
// Returns true on success, false if table end has been reached or error occurred.
// The caller should abort iteration on error.
func (it *tableIter[T]) Next() (ok bool) {
ok, it.err = it.getNext()
if ok && it.err != nil {
panic("unreachable")
}
return
}
// Index returns the current table index.
func (it *tableIter[T]) Index() uint16 {
return it.i - 1
}
// Err returns the current error.
func (it *tableIter[T]) Err() error {
return it.err
}
// Item returns the current element read.
//
// Next must be called before.
func (it *tableIter[T]) Item() T {
return it.elem
}
func (it *tableIter[T]) getNext() (bool, error) {
if it.i >= it.count {
return false, nil
}
if it.off >= math.MaxInt64 || it.off+uint64(it.elemSize) > math.MaxInt64 {
return false, io.ErrUnexpectedEOF
}
return nil
rd := io.NewSectionReader(it.l.rd, int64(it.off), int64(it.elemSize))
if err := binary.Read(rd, binary.LittleEndian, &it.elem); err != nil {
return false, err
}
it.off += uint64(it.elemSize)
it.i++
return true, nil
}
func isOverlap(startA uint64, sizeA uint64, startB uint64, sizeB uint64) bool {

View File

@ -48,6 +48,50 @@ func TestLoadProgram_Noop(t *testing.T) {
Memsz: 208,
Align: 4096,
},
ShStr: elf.Section64{},
ShShstrtab: elf.Section64{
Name: 82,
Type: uint32(elf.SHT_STRTAB),
Flags: 0,
Addr: 0,
Off: 8648,
Size: 100,
Addralign: 1,
},
ShSymtab: &elf.Section64{
Name: 74,
Type: uint32(elf.SHT_SYMTAB),
Flags: 0,
Addr: 0,
Off: 8504,
Size: 144,
Link: 12,
Info: 3,
Addralign: 8,
Entsize: 24,
},
ShStrtab: &elf.Section64{
Name: 92,
Type: uint32(elf.SHT_STRTAB),
Flags: 0,
Addr: 0,
Off: 8748,
Size: 39,
Link: 0,
Info: 0,
Addralign: 1,
Entsize: 0,
},
ShDynstr: &elf.Section64{
Name: 25,
Type: uint32(elf.SHT_STRTAB),
Flags: uint64(elf.DF_SYMBOLIC),
Addr: 624,
Off: 624,
Size: 23,
Link: 0,
Info: 0,
Addralign: 1,
Entsize: 0,
},
}, exe)
}