mirror of
https://github.com/mandiant/GoReSym
synced 2026-06-08 15:40:15 +00:00
78c02cc730
* clarify Go runtime version vs GoReSym layout version * Add Go 1.26 support * Cleanup debugging prints * Remove remaining debug print in main.go * Fix redundant nil check in pe.go * Final touches: fix sudo in build script and rename layout convention to 1.22 * Run go fmt * Revert unnecessary moduledata scanning changes in executable parsers * refactor fixes * better variable name for refactor * Fix lots of version issues and bugs in magic prefix check, as well as concurrent lock bug * fmt * Refactor slice, textsec, and more to use offsets. Cleanup dead code in internal.go * fmt --------- Co-authored-by: Stephen Eckels <stevemk14ebr@gmail.com>
461 lines
13 KiB
Go
461 lines
13 KiB
Go
// Copyright 2013 The Go Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
/*Copyright (C) 2022 Mandiant, Inc. All Rights Reserved.*/
|
|
|
|
// Parsing of Mach-O executables (OS X).
|
|
|
|
package objfile
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/binary"
|
|
"fmt"
|
|
"io"
|
|
"sort"
|
|
|
|
"github.com/mandiant/GoReSym/debug/dwarf"
|
|
"github.com/mandiant/GoReSym/debug/macho"
|
|
)
|
|
|
|
const stabTypeMask = 0xe0
|
|
|
|
type machoFile struct {
|
|
macho *macho.File
|
|
}
|
|
|
|
func openMacho(r io.ReaderAt) (rawFile, error) {
|
|
f, err := macho.NewFile(r)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &machoFile{f}, nil
|
|
}
|
|
|
|
func (f *machoFile) read_memory(VA uint64, size uint64) (data []byte, err error) {
|
|
for _, load := range f.macho.Loads {
|
|
seg, ok := load.(*macho.Segment)
|
|
if !ok {
|
|
continue
|
|
}
|
|
if seg.Addr <= VA && VA <= seg.Addr+seg.Filesz-1 {
|
|
if seg.Name == "__PAGEZERO" {
|
|
continue
|
|
}
|
|
n := seg.Addr + seg.Filesz - VA
|
|
if n > size {
|
|
n = size
|
|
}
|
|
data := make([]byte, n)
|
|
_, err := seg.ReadAt(data, int64(VA-seg.Addr))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return data, nil
|
|
}
|
|
}
|
|
return nil, fmt.Errorf("Failed to read memory")
|
|
}
|
|
|
|
func (f *machoFile) symbols() ([]Sym, error) {
|
|
if f.macho.Symtab == nil {
|
|
return nil, nil
|
|
}
|
|
|
|
// Build sorted list of addresses of all symbols.
|
|
// We infer the size of a symbol by looking at where the next symbol begins.
|
|
var addrs []uint64
|
|
for _, s := range f.macho.Symtab.Syms {
|
|
// Skip stab debug info.
|
|
if s.Type&stabTypeMask == 0 {
|
|
addrs = append(addrs, s.Value)
|
|
}
|
|
}
|
|
sort.Sort(uint64s(addrs))
|
|
|
|
var syms []Sym
|
|
for _, s := range f.macho.Symtab.Syms {
|
|
if s.Type&stabTypeMask != 0 {
|
|
// Skip stab debug info.
|
|
continue
|
|
}
|
|
sym := Sym{Name: s.Name, Addr: s.Value, Code: '?'}
|
|
i := sort.Search(len(addrs), func(x int) bool { return addrs[x] > s.Value })
|
|
if i < len(addrs) {
|
|
sym.Size = int64(addrs[i] - s.Value)
|
|
}
|
|
if s.Sect == 0 {
|
|
sym.Code = 'U'
|
|
} else if int(s.Sect) <= len(f.macho.Sections) {
|
|
sect := f.macho.Sections[s.Sect-1]
|
|
switch sect.Seg {
|
|
case "__TEXT", "__DATA_CONST":
|
|
sym.Code = 'R'
|
|
case "__DATA":
|
|
sym.Code = 'D'
|
|
}
|
|
switch sect.Seg + " " + sect.Name {
|
|
case "__TEXT __text":
|
|
sym.Code = 'T'
|
|
case "__DATA __bss", "__DATA __noptrbss":
|
|
sym.Code = 'B'
|
|
}
|
|
}
|
|
syms = append(syms, sym)
|
|
}
|
|
|
|
return syms, nil
|
|
}
|
|
|
|
func (f *machoFile) pcln_scan() (candidates <-chan PclntabCandidate, err error) {
|
|
// 1) Locate pclntab via symbols (standard way)
|
|
foundpcln := false
|
|
var pclntab []byte
|
|
if sect := f.macho.Section("__gopclntab"); sect != nil {
|
|
if pclntab, err = sect.Data(); err == nil {
|
|
foundpcln = true
|
|
}
|
|
}
|
|
|
|
pclntab_sigs_le := [][]byte{
|
|
[]byte("\xF1\xFF\xFF\xFF\x00\x00"), // little endian
|
|
[]byte("\xF0\xFF\xFF\xFF\x00\x00"),
|
|
[]byte("\xFA\xFF\xFF\xFF\x00\x00"),
|
|
[]byte("\xFB\xFF\xFF\xFF\x00\x00"),
|
|
}
|
|
|
|
pclntab_sigs_be := [][]byte{
|
|
[]byte("\xFF\xFF\xFF\xF1\x00\x00"), // big endian
|
|
[]byte("\xFF\xFF\xFF\xF0\x00\x00"),
|
|
[]byte("\xFF\xFF\xFF\xFA\x00\x00"),
|
|
[]byte("\xFF\xFF\xFF\xFB\x00\x00"),
|
|
}
|
|
|
|
var symtab []byte
|
|
var symtab_err error
|
|
if sect := f.macho.Section("__gosymtab"); sect != nil {
|
|
symtab, symtab_err = sect.Data()
|
|
}
|
|
|
|
// 2) if not found, byte scan for it
|
|
pclntab_sigs := append(pclntab_sigs_le, pclntab_sigs_be...)
|
|
ch_tab := make(chan PclntabCandidate)
|
|
|
|
send_tab := func(candidate *PclntabCandidate) {
|
|
if symtab_err != nil {
|
|
candidate.Symtab = symtab
|
|
ch_tab <- *candidate
|
|
}
|
|
ch_tab <- *candidate
|
|
}
|
|
|
|
send_patched_magic_candidates := func(candidate *PclntabCandidate) {
|
|
has_some_valid_magic := false
|
|
for _, magic := range append(pclntab_sigs_le, pclntab_sigs_be...) {
|
|
if bytes.HasPrefix(candidate.Pclntab, magic) {
|
|
has_some_valid_magic = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if !has_some_valid_magic {
|
|
for _, magic := range append(pclntab_sigs_le, pclntab_sigs_be...) {
|
|
pclntab_copy := make([]byte, len(candidate.Pclntab))
|
|
copy(pclntab_copy, candidate.Pclntab)
|
|
copy(pclntab_copy, magic)
|
|
|
|
new_candidate := candidate
|
|
new_candidate.Pclntab = pclntab_copy
|
|
send_tab(new_candidate)
|
|
}
|
|
}
|
|
}
|
|
|
|
send_stomped_magic_candidate := func(stompedMagicCandidate *StompMagicCandidate) {
|
|
for _, sec := range f.macho.Sections {
|
|
// malware can split the pclntab across multiple sections, re-merge
|
|
data := f.macho.DataAfterSection(sec)
|
|
pclntab_va_candidate := stompedMagicCandidate.PclntabVa
|
|
|
|
if pclntab_va_candidate >= sec.Addr && pclntab_va_candidate < (sec.Addr+sec.Size) && pclntab_va_candidate < (sec.Addr+uint64(len(data))) {
|
|
sec_offset := pclntab_va_candidate - sec.Addr
|
|
pclntab = data[sec_offset:]
|
|
|
|
if stompedMagicCandidate.LittleEndian {
|
|
for _, magicLE := range pclntab_sigs_le {
|
|
pclntab_copy := make([]byte, len(pclntab))
|
|
copy(pclntab_copy, pclntab)
|
|
copy(pclntab_copy, magicLE)
|
|
|
|
var candidate PclntabCandidate
|
|
candidate.StompMagicCandidateMeta = stompedMagicCandidate
|
|
candidate.Pclntab = pclntab_copy
|
|
candidate.SecStart = uint64(sec.Addr)
|
|
candidate.PclntabVA = pclntab_va_candidate
|
|
|
|
send_tab(&candidate)
|
|
}
|
|
} else {
|
|
for _, magicBE := range pclntab_sigs_be {
|
|
pclntab_copy := make([]byte, len(pclntab))
|
|
copy(pclntab_copy, pclntab)
|
|
copy(pclntab_copy, magicBE)
|
|
|
|
var candidate PclntabCandidate
|
|
candidate.StompMagicCandidateMeta = stompedMagicCandidate
|
|
candidate.Pclntab = pclntab_copy
|
|
candidate.SecStart = uint64(sec.Addr)
|
|
candidate.PclntabVA = pclntab_va_candidate
|
|
|
|
send_tab(&candidate)
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
// 2) if not found, byte scan for it
|
|
go func() {
|
|
defer close(ch_tab)
|
|
for _, sec := range f.macho.Sections {
|
|
// malware can split the pclntab across multiple sections, re-merge
|
|
data := f.macho.DataAfterSection(sec)
|
|
|
|
if !foundpcln {
|
|
matches := findAllOccurrences(data, pclntab_sigs)
|
|
for _, pclntab_idx := range matches {
|
|
if pclntab_idx != -1 && pclntab_idx < int(sec.Size) {
|
|
pclntab = data[pclntab_idx:]
|
|
|
|
var candidate PclntabCandidate
|
|
candidate.Pclntab = pclntab
|
|
|
|
candidate.SecStart = uint64(sec.Addr)
|
|
candidate.PclntabVA = candidate.SecStart + uint64(pclntab_idx)
|
|
|
|
send_patched_magic_candidates(&candidate)
|
|
send_tab(&candidate)
|
|
// we must scan all signature for all sections. DO NOT BREAK
|
|
}
|
|
}
|
|
} else {
|
|
// 3) if we found it earlier, figure out which section base to return (might be wrong for packed things)
|
|
pclntab_idx := bytes.Index(data, pclntab)
|
|
if pclntab_idx != -1 {
|
|
var candidate PclntabCandidate
|
|
candidate.Pclntab = pclntab
|
|
|
|
candidate.SecStart = uint64(sec.Addr)
|
|
candidate.PclntabVA = candidate.SecStart + uint64(pclntab_idx)
|
|
|
|
send_patched_magic_candidates(&candidate)
|
|
send_tab(&candidate)
|
|
}
|
|
}
|
|
|
|
// 4) Always try this other way! Sometimes the pclntab magic is stomped as well so our byte OR symbol location fail. Byte scan for the moduledata, use that to find the pclntab instead, fix up magic with all combinations.
|
|
// See the obfuscator 'garble' for an example of randomizing the pclntab magic
|
|
sigResults := findModuleInitPCHeader(data, sec.Addr)
|
|
for _, sigResult := range sigResults {
|
|
// example: off_69D0C0 is the moduleData we found via our scan, the first ptr unk_5DF6E0, is the pclntab!
|
|
// 0x000000000069D0C0 E0 F6 5D 00 00 00 00 00 off_69D0C0 dq offset unk_5DF6E0 ; DATA XREF: runtime_SetFinalizer+119↑o
|
|
// 0x000000000069D0C0 ; runtime_scanstack+40B↑o ...
|
|
// 0x000000000069D0C8 40 F7 5D 00 00 00 00 00 dq offset aInternalCpuIni ; "internal/cpu.Initialize"
|
|
// 0x000000000069D0D0 F0 db 0F0h
|
|
// 0x000000000069D0D1 BB db 0BBh
|
|
|
|
// we don't know the endianess or arch, so we submit all combinations as candidates and sort them out later
|
|
// example: reads out ptr unk_5DF6E0
|
|
pclntabVARaw64, err := f.read_memory(sigResult.moduleDataVA, 8) // assume 64bit
|
|
if err == nil {
|
|
stompedMagicCandidateLE := StompMagicCandidate{
|
|
binary.LittleEndian.Uint64(pclntabVARaw64),
|
|
sigResult.moduleDataVA,
|
|
true,
|
|
}
|
|
stompedMagicCandidateBE := StompMagicCandidate{
|
|
binary.BigEndian.Uint64(pclntabVARaw64),
|
|
sigResult.moduleDataVA,
|
|
false,
|
|
}
|
|
send_stomped_magic_candidate(&stompedMagicCandidateBE)
|
|
send_stomped_magic_candidate(&stompedMagicCandidateLE)
|
|
}
|
|
|
|
pclntabVARaw32, err := f.read_memory(sigResult.moduleDataVA, 4) // assume 32bit
|
|
if err == nil {
|
|
stompedMagicCandidateLE := StompMagicCandidate{
|
|
uint64(binary.LittleEndian.Uint32(pclntabVARaw32)),
|
|
sigResult.moduleDataVA,
|
|
true,
|
|
}
|
|
stompedMagicCandidateBE := StompMagicCandidate{
|
|
uint64(binary.BigEndian.Uint32(pclntabVARaw32)),
|
|
sigResult.moduleDataVA,
|
|
false,
|
|
}
|
|
send_stomped_magic_candidate(&stompedMagicCandidateBE)
|
|
send_stomped_magic_candidate(&stompedMagicCandidateLE)
|
|
}
|
|
}
|
|
}
|
|
}()
|
|
return ch_tab, nil
|
|
}
|
|
|
|
func (f *machoFile) pcln() (candidates <-chan PclntabCandidate, err error) {
|
|
candidates, err = f.pcln_scan()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return candidates, nil
|
|
}
|
|
|
|
func (f *machoFile) moduledata_scan(pclntabVA uint64, is64bit bool, littleendian bool, ignorelist []uint64) (candidate *ModuleDataCandidate, err error) {
|
|
found := false
|
|
|
|
var secStart uint64
|
|
var moduledata []uint8
|
|
var moduledataVA uint64
|
|
scan:
|
|
for _, sec := range f.macho.Sections {
|
|
// malware can split the pclntab across multiple sections, re-merge
|
|
data := f.macho.DataAfterSection(sec)
|
|
// fall back to scanning for structure using address of pclntab, which is first value in struc
|
|
var pclntabVA_bytes []byte
|
|
if is64bit {
|
|
pclntabVA_bytes = make([]byte, 8)
|
|
if littleendian {
|
|
binary.LittleEndian.PutUint64(pclntabVA_bytes, pclntabVA)
|
|
} else {
|
|
binary.BigEndian.PutUint64(pclntabVA_bytes, pclntabVA)
|
|
}
|
|
} else {
|
|
pclntabVA_bytes = make([]byte, 4)
|
|
if littleendian {
|
|
binary.LittleEndian.PutUint32(pclntabVA_bytes, uint32(pclntabVA))
|
|
} else {
|
|
binary.BigEndian.PutUint32(pclntabVA_bytes, uint32(pclntabVA))
|
|
}
|
|
}
|
|
|
|
moduledata_idx := bytes.Index(data, pclntabVA_bytes)
|
|
if moduledata_idx != -1 && moduledata_idx < int(sec.Size) {
|
|
moduledata = data[moduledata_idx:]
|
|
moduledataVA = sec.Addr + uint64(moduledata_idx)
|
|
secStart = sec.Addr
|
|
|
|
// optionally consult ignore list, to skip past previous (bad) scan results
|
|
for _, ignore := range ignorelist {
|
|
if ignore == moduledataVA {
|
|
continue scan
|
|
}
|
|
}
|
|
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if !found {
|
|
return nil, fmt.Errorf("moduledata containing section could not be located")
|
|
}
|
|
|
|
return &ModuleDataCandidate{SecStart: secStart, ModuledataVA: moduledataVA, Moduledata: moduledata}, nil
|
|
}
|
|
|
|
func (f *machoFile) text() (textStart uint64, text []byte, err error) {
|
|
sect := f.macho.Section("__text")
|
|
if sect == nil {
|
|
return 0, nil, fmt.Errorf("text section not found")
|
|
}
|
|
textStart = sect.Addr
|
|
text, err = sect.Data()
|
|
return
|
|
}
|
|
|
|
func (f *machoFile) rdata() (textStart uint64, text []byte, err error) {
|
|
sect := f.macho.Section("__DATA")
|
|
if sect == nil {
|
|
return 0, nil, fmt.Errorf("data section not found")
|
|
}
|
|
textStart = sect.Addr
|
|
text, err = sect.Data()
|
|
return
|
|
}
|
|
|
|
func (f *machoFile) goarch() string {
|
|
switch f.macho.Cpu {
|
|
case macho.Cpu386:
|
|
return "386"
|
|
case macho.CpuAmd64:
|
|
return "amd64"
|
|
case macho.CpuArm:
|
|
return "arm"
|
|
case macho.CpuArm64:
|
|
return "arm64"
|
|
case macho.CpuPpc64:
|
|
return "ppc64"
|
|
}
|
|
return ""
|
|
}
|
|
|
|
type uint64s []uint64
|
|
|
|
func (x uint64s) Len() int { return len(x) }
|
|
func (x uint64s) Swap(i, j int) { x[i], x[j] = x[j], x[i] }
|
|
func (x uint64s) Less(i, j int) bool { return x[i] < x[j] }
|
|
|
|
func (f *machoFile) loadAddress() (uint64, error) {
|
|
if seg := f.macho.Segment("__TEXT"); seg != nil {
|
|
return seg.Addr, nil
|
|
}
|
|
return 0, fmt.Errorf("unknown load address")
|
|
}
|
|
|
|
func (f *machoFile) dwarf() (*dwarf.Data, error) {
|
|
return f.macho.DWARF()
|
|
}
|
|
|
|
// iterateSections calls the provided function for each section.
|
|
// This avoids loading all section data into memory at once.
|
|
func (f *machoFile) iterateSections(fn func(Section) error) error {
|
|
for _, sec := range f.macho.Sections {
|
|
data, err := sec.Data()
|
|
if err != nil {
|
|
// Skip sections we can't read
|
|
continue
|
|
}
|
|
section := Section{
|
|
Name: sec.Name,
|
|
Addr: sec.Addr,
|
|
Data: data,
|
|
}
|
|
if err := fn(section); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// is64Bit returns true if this is a 64-bit Mach-O binary.
|
|
// Go's debug/macho package represents CPU type directly on the File struct.
|
|
func (f *machoFile) is64Bit() bool {
|
|
switch f.macho.Cpu {
|
|
case macho.CpuAmd64, macho.CpuArm64:
|
|
return true
|
|
default:
|
|
return false // CpuI386, CpuArm, CpuPpc = 32-bit
|
|
}
|
|
}
|
|
|
|
// isLittleEndian returns true if this Mach-O binary is little-endian.
|
|
// Modern Macs (x86-64, ARM64) are little-endian.
|
|
// Old PowerPC Macs were big-endian.
|
|
func (f *machoFile) isLittleEndian() bool {
|
|
return f.macho.ByteOrder == binary.LittleEndian
|
|
}
|