mirror of
https://github.com/lukaszraczylo/go-obfuscation.git
synced 2026-10-12 05:32:33 +00:00
Bug fixes: - pclntab/rtcorrupt_linux.go: remove duplicate mprotectRW/mprotectRO that broke GOOS=linux builds (they're defined in rtcorrupt_unsafe.go for darwin+linux). - pclntab/pclntab.go: readUint now guards against negative offset (previously panicked via slice out-of-range). - integrity/integrity.go: SetExpectedHashHex now validates length 64 up-front and reports byte index on parse error; added ClearExpectedHash for runtime toggling and test isolation. - transform/mba.go: mbaApplyBinOp used strings.Index on first match instead of the current match; replaced with FindAllStringSubmatchIndex + two-pass replacement. - transform/transform.go: removed duplicate unconditional SplitLongStrings call. New transform passes (CLI flags + Config fields + pipeline wiring): - splice, constblind, mba, fakesigs, funcsplit, bbreorder — all default true. Cleanup: - strenc/keyderive.go: removed unused DeriveFromBinary + buildSeed. - transform/transform.go: removed unused trimFirstParen. - antidebug_hook_darwin.go: modernized readAddr to unsafe.Slice with noescape pattern (vet-safe). Tests (10 packages, 116 tests, all green): - internal/transform: 28 tests covering every pipeline stage. - pkg/strenc: 7 tests for key derivation + XOR stream. - pkg/stringcrypt: 7 tests for CFB round-trip + tamper detection. - pkg/pageman: 14 tests for alignment + mprotect guards. - pkg/integrity: 14 tests including ClearExpectedHash teardown. - pkg/pclntab: 12 tests including readUint negative-offset guard. - pkg/antisandbox: 6 tests for hostname/MAC lists + Check delegation. - pkg/antiemul: 6 tests (1 skipped on non-Linux) for no-panic + timing. Tooling: - Makefile: added test, verify, lint, fmt, vet targets. vet uses -unsafeptr=false (only legitimate uintptr→pointer is the darwin antidebug hook). fmt skips build/ (generated). - README.md: test coverage table + Makefile target reference.
278 lines
5.3 KiB
Go
278 lines
5.3 KiB
Go
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 < 0 || 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
|
|
}
|