This commit is contained in:
2026-05-31 19:48:56 +01:00
parent 7f32417736
commit 68f8904282
39 changed files with 4519 additions and 5 deletions
+277
View File
@@ -0,0 +1,277 @@
package pclntab
import (
"crypto/rand"
"encoding/binary"
"fmt"
)
var pclntabMagics = [][]byte{
{0xF1, 0xFF, 0xFF, 0xFA}, // Go 1.2 - 1.15
{0xF1, 0xFF, 0xFF, 0xF0}, // Go 1.16 - 1.17
{0xF1, 0xFF, 0xFF, 0xF1}, // Go 1.18+
}
type CorruptOptions struct {
EncryptNames bool
ScrambleMagic bool
InjectFake bool
Key byte
}
func CorruptPclntab(binaryPath string, opts CorruptOptions) ([]byte, error) {
if opts.Key == 0 {
var k [1]byte
if _, err := rand.Read(k[:]); err != nil {
return nil, fmt.Errorf("pclntab: failed to generate key: %w", err)
}
if k[0] == 0 {
k[0] = 0x42
}
opts.Key = k[0]
}
data, err := readPclntabSection(binaryPath)
if err != nil {
return nil, err
}
if data == nil {
return nil, fmt.Errorf("pclntab: .gopclntab section not found in %s", binaryPath)
}
corrupted := make([]byte, len(data))
copy(corrupted, data)
tab := &pclntabParser{data: corrupted}
if opts.ScrambleMagic {
scrambleMagic(corrupted)
}
if opts.EncryptNames {
if err := encryptNames(tab, opts.Key); err != nil {
return nil, fmt.Errorf("pclntab: failed to encrypt names: %w", err)
}
}
if opts.InjectFake {
corrupted = injectFakeEntries(corrupted)
}
return corrupted, nil
}
type pclntabParser struct {
data []byte
magic []byte
quantum uint8
ptrSize uint8
nfiles uint32
nameOff uint32
strtabOff uint32
valid bool
}
func (p *pclntabParser) parse() error {
if len(p.data) < 16 {
return fmt.Errorf("pclntab: data too small (%d bytes)", len(p.data))
}
p.magic = p.data[:4]
found := false
for _, m := range pclntabMagics {
if bytesEqual(p.magic, m) {
found = true
break
}
}
if !found {
return fmt.Errorf("pclntab: unrecognized magic: %x", p.magic)
}
p.ptrSize = p.data[6]
p.quantum = p.data[7]
if p.ptrSize == 0 || (p.ptrSize != 4 && p.ptrSize != 8) {
p.ptrSize = 8
}
if p.quantum == 0 {
p.quantum = 1
}
if bytesEqual(p.magic, pclntabMagics[2]) {
if len(p.data) >= 20 {
p.nfiles = binary.LittleEndian.Uint32(p.data[16:20])
}
}
p.valid = true
return nil
}
func scrambleMagic(data []byte) {
if len(data) < 4 {
return
}
var rnd [4]byte
_, _ = rand.Read(rnd[:])
for i := 0; i < 4; i++ {
data[i] = rnd[i]
}
}
func encryptNames(tab *pclntabParser, key byte) error {
if err := tab.parse(); err != nil {
return err
}
nameStart, nameEnd := findNameRegion(tab)
if nameStart == 0 || nameEnd == 0 || nameStart >= nameEnd {
return fmt.Errorf("pclntab: could not locate name region")
}
if nameEnd > uint32(len(tab.data)) {
nameEnd = uint32(len(tab.data))
}
for i := nameStart; i < nameEnd; i++ {
if tab.data[i] != 0 {
tab.data[i] ^= key
}
}
return nil
}
func findNameRegion(tab *pclntabParser) (uint32, uint32) {
data := tab.data
ptrSize := int(tab.ptrSize)
if bytesEqual(tab.magic, pclntabMagics[2]) {
if len(data) < 16+4+4*int(ptrSize) {
return 0, 0
}
offset := 16 + 4
_ = readUint(data, offset, ptrSize) // nfunc
offset += ptrSize
offset += ptrSize // nfiles (Go 1.18 already read above)
_ = readUint(data, offset, ptrSize) // textStart
offset += ptrSize
funcnametab := readUint(data, offset, ptrSize)
offset += ptrSize
_ = readUint(data, offset, ptrSize) // cutab
offset += ptrSize
_ = readUint(data, offset, ptrSize) // filetab
offset += ptrSize
_ = readUint(data, offset, ptrSize) // pctab
offset += ptrSize
pclntab := readUint(data, offset, ptrSize)
_ = pclntab
nameStart := uint32(funcnametab)
nameEnd := nameStart
for i := nameStart; i < uint32(len(data)); i++ {
if data[i] == 0 {
if i+1 < uint32(len(data)) && data[i+1] == 0 {
nameEnd = i
break
}
}
}
if nameEnd == nameStart {
nameEnd = uint32(len(data))
}
return nameStart, nameEnd
}
return findNameRegionLegacy(tab)
}
func findNameRegionLegacy(tab *pclntabParser) (uint32, uint32) {
data := tab.data
offset := 8 + 4 + 4
if bytesEqual(tab.magic, pclntabMagics[1]) {
offset += 4
}
if offset+8 > len(data) {
return 0, 0
}
nameOff := binary.LittleEndian.Uint32(data[offset : offset+4])
_ = binary.LittleEndian.Uint32(data[offset+4 : offset+8])
nameStart := nameOff
nameEnd := nameStart
for i := nameStart; i < uint32(len(data)); i++ {
if data[i] == 0 {
if i+1 < uint32(len(data)) && data[i+1] == 0 {
nameEnd = i
break
}
}
}
if nameEnd == nameStart {
nameEnd = uint32(len(data))
}
return nameStart, nameEnd
}
func injectFakeEntries(data []byte) []byte {
fakeEntries := make([]byte, 256)
_, _ = rand.Read(fakeEntries)
fakeNames := []string{
"runtime.fakeGoroutine",
"crypto/internal.init",
"main.processRequest",
"net/http.handleConn",
"google.golang.org/grpc.recvMsg",
"github.com/user/repo.processData",
}
offset := 0
for _, name := range fakeNames {
if offset+len(name)+1 > len(fakeEntries) {
break
}
copy(fakeEntries[offset:], name)
offset += len(name) + 1
}
return append(data, fakeEntries...)
}
func readUint(data []byte, offset int, size int) uint64 {
if offset+size > len(data) {
return 0
}
switch size {
case 4:
return uint64(binary.LittleEndian.Uint32(data[offset : offset+4]))
case 8:
return binary.LittleEndian.Uint64(data[offset : offset+8])
default:
return 0
}
}
func bytesEqual(a, b []byte) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if a[i] != b[i] {
return false
}
}
return true
}
+57
View File
@@ -0,0 +1,57 @@
//go:build linux
package pclntab
import (
"debug/elf"
"fmt"
)
func readPclntabSection(binaryPath string) ([]byte, error) {
f, err := elf.Open(binaryPath)
if err != nil {
return nil, fmt.Errorf("pclntab: failed to open ELF: %w", err)
}
defer f.Close()
sectionNames := []string{
".gopclntab",
"runtime.pclntab",
"runtime.pclntab_v1",
}
for _, name := range sectionNames {
section := f.Section(name)
if section != nil {
data, err := section.Data()
if err != nil {
return nil, fmt.Errorf("pclntab: failed to read section %s: %w", name, err)
}
buf := make([]byte, len(data))
copy(buf, data)
return buf, nil
}
}
for _, section := range f.Sections {
data, err := section.Data()
if err != nil {
continue
}
if len(data) >= 8 {
for _, magic := range pclntabMagics {
if len(data) >= len(magic) && bytesEqual(data[:len(magic)], magic) {
buf := make([]byte, len(data))
copy(buf, data)
return buf, nil
}
}
}
}
return nil, nil
}
func WriteCorruptedPclntab(binaryPath string, corrupted []byte, outputPath string) error {
return fmt.Errorf("pclntab: offline ELF patching not yet implemented; use runtime corruption instead")
}
+57
View File
@@ -0,0 +1,57 @@
//go:build darwin
package pclntab
import (
"debug/macho"
"fmt"
)
func readPclntabSection(binaryPath string) ([]byte, error) {
f, err := macho.Open(binaryPath)
if err != nil {
return nil, fmt.Errorf("pclntab: failed to open Mach-O: %w", err)
}
defer f.Close()
sectionNames := []string{
"__gopclntab",
"__pclntab",
}
for _, section := range f.Sections {
for _, name := range sectionNames {
if section.Name == name {
data, err := section.Data()
if err != nil {
return nil, fmt.Errorf("pclntab: failed to read section %s: %w", name, err)
}
buf := make([]byte, len(data))
copy(buf, data)
return buf, nil
}
}
}
for _, section := range f.Sections {
data, err := section.Data()
if err != nil {
continue
}
if len(data) >= 8 {
for _, magic := range pclntabMagics {
if len(data) >= len(magic) && bytesEqual(data[:len(magic)], magic) {
buf := make([]byte, len(data))
copy(buf, data)
return buf, nil
}
}
}
}
return nil, nil
}
func WriteCorruptedPclntab(binaryPath string, corrupted []byte, outputPath string) error {
return fmt.Errorf("pclntab: offline Mach-O patching not yet implemented; use runtime corruption instead")
}
+58
View File
@@ -0,0 +1,58 @@
//go:build windows
package pclntab
import (
"debug/pe"
"fmt"
)
func readPclntabSection(binaryPath string) ([]byte, error) {
f, err := pe.Open(binaryPath)
if err != nil {
return nil, fmt.Errorf("pclntab: failed to open PE: %w", err)
}
defer f.Close()
sectionNames := []string{
".gopclntab",
"runtime.pclntab",
"runtime.pclntab_v1",
}
for _, section := range f.Sections {
for _, name := range sectionNames {
if section.Name == name {
data, err := section.Data()
if err != nil {
return nil, fmt.Errorf("pclntab: failed to read section %s: %w", name, err)
}
buf := make([]byte, len(data))
copy(buf, data)
return buf, nil
}
}
}
for _, section := range f.Sections {
data, err := section.Data()
if err != nil {
continue
}
if len(data) >= 8 {
for _, magic := range pclntabMagics {
if len(data) >= len(magic) && bytesEqual(data[:len(magic)], magic) {
buf := make([]byte, len(data))
copy(buf, data)
return buf, nil
}
}
}
}
return nil, nil
}
func WriteCorruptedPclntab(binaryPath string, corrupted []byte, outputPath string) error {
return fmt.Errorf("pclntab: offline PE patching not yet implemented; use runtime corruption instead")
}
+33
View File
@@ -0,0 +1,33 @@
package pclntab
import (
"crypto/rand"
"fmt"
"os"
)
// RuntimeCorrupt finds the running binary's pclntab in memory and XOR-encrypts
// all name strings. Call this during init() — the Go runtime has already used
// pclntab to resolve function pointers, so the program keeps working but
// post-init reverse engineering tools see garbage names.
//
// WARNING: Degrades panic stack traces (function names become unreadable).
func RuntimeCorrupt(key byte) error {
if key == 0 {
var k [1]byte
if _, err := rand.Read(k[:]); err != nil {
return fmt.Errorf("pclntab: failed to generate key: %w", err)
}
if k[0] == 0 {
k[0] = 0x42
}
key = k[0]
}
selfPath, err := os.Executable()
if err != nil {
return fmt.Errorf("pclntab: cannot determine executable: %w", err)
}
return runtimeCorrupt(selfPath, key)
}
+116
View File
@@ -0,0 +1,116 @@
//go:build darwin
package pclntab
import (
"debug/macho"
"fmt"
"os"
"syscall"
)
func runtimeCorrupt(binaryPath string, key byte) error {
self, err := os.ReadFile(binaryPath)
if err != nil {
return fmt.Errorf("pclntab: cannot read binary: %w", err)
}
f, err := macho.NewFile(bytesReader(self))
if err != nil {
return fmt.Errorf("pclntab: cannot parse Mach-O from memory: %w", err)
}
sectionData, sectionAddr, sectionSize, err := findPclntabMacho(f)
if err != nil {
return err
}
nameStart, nameEnd := findMemoryNameRegion(sectionData)
if nameStart == 0 || nameEnd <= nameStart {
return fmt.Errorf("pclntab: could not locate name region")
}
pageSize := syscall.Getpagesize()
baseAddr := sectionAddr &^ uintptr(pageSize-1)
protSize := sectionSize + (sectionAddr - baseAddr)
if err := mprotectRW(baseAddr, protSize); err != nil {
return fmt.Errorf("pclntab: mprotect failed: %w", err)
}
nameSlice := ptrToSlice(sectionAddr+uintptr(nameStart), uintptr(nameEnd-nameStart))
for i := range nameSlice {
if nameSlice[i] != 0 {
nameSlice[i] ^= key
}
}
mprotectRO(baseAddr, protSize)
return nil
}
func findPclntabMacho(f *macho.File) ([]byte, uintptr, uintptr, error) {
sectionNames := []string{
"__gopclntab",
"__pclntab",
}
for _, sec := range f.Sections {
for _, name := range sectionNames {
if sec.Name == name {
data, err := sec.Data()
if err != nil {
continue
}
buf := make([]byte, len(data))
copy(buf, data)
return buf, uintptr(sec.Addr), uintptr(sec.Size), nil
}
}
}
for _, sec := range f.Sections {
data, err := sec.Data()
if err != nil {
continue
}
if len(data) >= 8 {
for _, magic := range pclntabMagics {
if len(data) >= len(magic) && bytesEqual(data[:len(magic)], magic) {
buf := make([]byte, len(data))
copy(buf, data)
return buf, uintptr(sec.Addr), uintptr(sec.Size), nil
}
}
}
}
return nil, 0, 0, fmt.Errorf("pclntab: __gopclntab section not found in memory")
}
func findMemoryNameRegion(data []byte) (uint32, uint32) {
tab := &pclntabParser{data: data}
if err := tab.parse(); err != nil {
return 0, 0
}
return findNameRegion(tab)
}
type bytesReaderWrapper struct {
data []byte
}
func bytesReader(data []byte) *bytesReaderWrapper {
return &bytesReaderWrapper{data: data}
}
func (r *bytesReaderWrapper) ReadAt(p []byte, off int64) (int, error) {
if off < 0 || int(off) >= len(r.data) {
return 0, fmt.Errorf("invalid offset")
}
n := copy(p, r.data[int(off):])
if n < len(p) {
return n, fmt.Errorf("EOF")
}
return n, nil
}
+133
View File
@@ -0,0 +1,133 @@
//go:build linux
package pclntab
import (
"debug/elf"
"fmt"
"os"
"syscall"
"unsafe"
)
func runtimeCorrupt(binaryPath string, key byte) error {
self, err := os.ReadFile("/proc/self/exe")
if err != nil {
return fmt.Errorf("pclntab: cannot read /proc/self/exe: %w", err)
}
f, err := elf.NewFile(bytesReader(self))
if err != nil {
return fmt.Errorf("pclntab: cannot parse ELF from memory: %w", err)
}
sectionData, sectionAddr, sectionSize, err := findPclntabELF(f)
if err != nil {
return err
}
nameStart, nameEnd := findMemoryNameRegion(sectionData)
if nameStart == 0 || nameEnd <= nameStart {
return fmt.Errorf("pclntab: could not locate name region")
}
baseAddr := sectionAddr &^ uintptr(syscall.Getpagesize()-1)
protSize := sectionSize + (sectionAddr - baseAddr)
if err := mprotectRW(baseAddr, protSize); err != nil {
return fmt.Errorf("pclntab: mprotect failed: %w", err)
}
basePtr := unsafe.Pointer(sectionAddr)
namePtr := unsafe.Add(basePtr, nameStart)
nameSlice := (*[1 << 30]byte)(namePtr)[: nameEnd-nameStart : nameEnd-nameStart]
for i := range nameSlice {
if nameSlice[i] != 0 {
nameSlice[i] ^= key
}
}
mprotectRO(baseAddr, protSize)
return nil
}
func findPclntabELF(f *elf.File) ([]byte, uintptr, uintptr, error) {
sectionNames := []string{
".gopclntab",
"runtime.pclntab",
"runtime.pclntab_v1",
}
for _, name := range sectionNames {
sec := f.Section(name)
if sec == nil {
continue
}
data, err := sec.Data()
if err != nil {
continue
}
buf := make([]byte, len(data))
copy(buf, data)
return buf, uintptr(sec.Addr), uintptr(sec.Size), nil
}
for _, sec := range f.Sections {
data, err := sec.Data()
if err != nil {
continue
}
if len(data) >= 8 {
for _, magic := range pclntabMagics {
if len(data) >= len(magic) && bytesEqual(data[:len(magic)], magic) {
buf := make([]byte, len(data))
copy(buf, data)
return buf, uintptr(sec.Addr), uintptr(sec.Size), nil
}
}
}
}
return nil, 0, 0, fmt.Errorf("pclntab: .gopclntab section not found in memory")
}
func mprotectRW(addr uintptr, size uintptr) error {
return syscall.Mprotect(
(*[1]byte)(unsafe.Pointer(addr))[:size:size],
syscall.PROT_READ|syscall.PROT_WRITE,
)
}
func mprotectRO(addr uintptr, size uintptr) {
_ = syscall.Mprotect(
(*[1]byte)(unsafe.Pointer(addr))[:size:size],
syscall.PROT_READ,
)
}
func findMemoryNameRegion(data []byte) (uint32, uint32) {
tab := &pclntabParser{data: data}
if err := tab.parse(); err != nil {
return 0, 0
}
return findNameRegion(tab)
}
type bytesReaderWrapper struct {
data []byte
}
func bytesReader(data []byte) *bytesReaderWrapper {
return &bytesReaderWrapper{data: data}
}
func (r *bytesReaderWrapper) ReadAt(p []byte, off int64) (int, error) {
if off < 0 || int(off) >= len(r.data) {
return 0, fmt.Errorf("invalid offset")
}
n := copy(p, r.data[int(off):])
if n < len(p) {
return n, fmt.Errorf("EOF")
}
return n, nil
}
+35
View File
@@ -0,0 +1,35 @@
//go:build darwin || linux
package pclntab
import (
"syscall"
"unsafe"
)
// mprotectRW makes a memory region read-write using an unsafe.Pointer conversion
// from a page-aligned uintptr. This is intentional for runtime memory manipulation.
//
//go:nosplit
func mprotectRW(addr uintptr, size uintptr) error {
p := *(*unsafe.Pointer)(unsafe.Pointer(&addr))
return syscall.Mprotect(
unsafe.Slice((*byte)(p), size),
syscall.PROT_READ|syscall.PROT_WRITE,
)
}
//go:nosplit
func mprotectRO(addr uintptr, size uintptr) {
p := *(*unsafe.Pointer)(unsafe.Pointer(&addr))
_ = syscall.Mprotect(
unsafe.Slice((*byte)(p), size),
syscall.PROT_READ,
)
}
//go:nosplit
func ptrToSlice(addr uintptr, size uintptr) []byte {
p := *(*unsafe.Pointer)(unsafe.Pointer(&addr))
return unsafe.Slice((*byte)(p), size)
}
+9
View File
@@ -0,0 +1,9 @@
//go:build windows
package pclntab
import "fmt"
func runtimeCorrupt(binaryPath string, key byte) error {
return fmt.Errorf("pclntab: runtime corruption not supported on windows")
}