github.com/eh-steve/goloader@v0.0.0-20240111193454-90ff3cfdae39/macho_darwin.go (about)

     1  //go:build darwin && !no_darwin_patch
     2  // +build darwin,!no_darwin_patch
     3  
     4  package goloader
     5  
     6  import (
     7  	"bytes"
     8  	"debug/macho"
     9  	"encoding/binary"
    10  	"fmt"
    11  	"io"
    12  	"log"
    13  	"math/rand"
    14  	"os"
    15  	"os/exec"
    16  	"runtime"
    17  	"syscall"
    18  	"unsafe"
    19  )
    20  
    21  func init() {
    22  	path, err := os.Executable()
    23  	if err != nil {
    24  		panic(fmt.Errorf("could not find executable path: %w", err))
    25  	}
    26  
    27  	// This is somewhat insane, but hey ¯\_(ツ)_/¯
    28  	havePatched, err := PatchMachoSelfMakeWriteable(path)
    29  	if err != nil {
    30  		panic(err)
    31  	}
    32  	if havePatched {
    33  		// Replace ourselves with the newly patched binary
    34  		// Since this is inside the init function, there shouldn't be too much program state built up...
    35  		log.Printf("patched Mach-O __TEXT segment to make writeable, restarting\n")
    36  		if runtime.GOARCH == "arm64" {
    37  			// Kernel caches code signing info for an inode so you have to replace it entirely
    38  			f, err := os.OpenFile(fmt.Sprintf("jit_bin_%x", rand.Uint64()), os.O_CREATE|os.O_WRONLY, 0755)
    39  			if err != nil {
    40  				panic(err)
    41  			}
    42  			orig, err := os.OpenFile(path, os.O_RDONLY, 0755)
    43  			if err != nil {
    44  				panic(err)
    45  			}
    46  			_, err = io.Copy(f, orig)
    47  			if err != nil {
    48  				panic(err)
    49  			}
    50  			err = f.Close()
    51  			if err != nil {
    52  				panic(err)
    53  			}
    54  			// What is the point of code signing if you can just do this?
    55  			err = exec.Command("codesign", "-s", "-", f.Name()).Run()
    56  			if err != nil {
    57  				panic(err)
    58  			}
    59  			err = os.Rename(f.Name(), path)
    60  			if err != nil {
    61  				panic(err)
    62  			}
    63  		}
    64  		err = syscall.Exec(os.Args[0], os.Args, os.Environ())
    65  		if err != nil {
    66  			panic(err)
    67  		}
    68  	}
    69  }
    70  
    71  const Write = 2
    72  
    73  const (
    74  	fileHeaderSize32 = 7 * 4
    75  	fileHeaderSize64 = 8 * 4
    76  )
    77  
    78  func PatchMachoSelfMakeWriteable(path string) (bool, error) {
    79  	r, err := os.OpenFile(path, os.O_RDWR, 0755)
    80  
    81  	if err != nil {
    82  		return false, fmt.Errorf("could not open file %s: %w", path, err)
    83  	}
    84  	sr := io.NewSectionReader(r, 0, 1<<63-1)
    85  
    86  	var ident [4]byte
    87  	if _, err := r.ReadAt(ident[0:], 0); err != nil {
    88  		return false, fmt.Errorf("could not read first 4 bytes of file %s: %w", path, err)
    89  	}
    90  
    91  	be := binary.BigEndian.Uint32(ident[0:])
    92  	le := binary.LittleEndian.Uint32(ident[0:])
    93  	var magic uint32
    94  	var bo binary.ByteOrder
    95  	switch macho.Magic32 &^ 1 {
    96  	case be &^ 1:
    97  		bo = binary.BigEndian
    98  		magic = be
    99  	case le &^ 1:
   100  		bo = binary.LittleEndian
   101  		magic = le
   102  	default:
   103  		return false, fmt.Errorf("invalid magic number 0x%x", magic)
   104  	}
   105  
   106  	header := macho.FileHeader{}
   107  	if err := binary.Read(sr, bo, &header); err != nil {
   108  		return false, fmt.Errorf("could not read macho file header of file %s: %w", path, err)
   109  	}
   110  
   111  	offset := int64(fileHeaderSize32)
   112  	if magic == macho.Magic64 {
   113  		offset = fileHeaderSize64
   114  	}
   115  
   116  	dat := make([]byte, header.Cmdsz)
   117  	if _, err := r.ReadAt(dat, offset); err != nil {
   118  		return false, fmt.Errorf("failed to read macho command data: %w", err)
   119  	}
   120  
   121  	var havePatched = false
   122  	for i := 0; i < int(header.Ncmd); i++ {
   123  		if len(dat) < 8 {
   124  			return false, fmt.Errorf("command block too small")
   125  		}
   126  		cmd, siz := macho.LoadCmd(bo.Uint32(dat[0:4])), bo.Uint32(dat[4:8])
   127  		if siz < 8 || siz > uint32(len(dat)) {
   128  			return false, fmt.Errorf("invalid command block size")
   129  		}
   130  		var cmddat []byte
   131  		cmddat, dat = dat[0:siz], dat[siz:]
   132  
   133  		switch cmd {
   134  		case macho.LoadCmdSegment:
   135  			var seg32 macho.Segment32
   136  			b := bytes.NewReader(cmddat)
   137  			if err := binary.Read(b, bo, &seg32); err != nil {
   138  				return false, fmt.Errorf("failed to read LoadCmdSegment: %w", err)
   139  			}
   140  			if cstring(seg32.Name[0:]) == "__TEXT" {
   141  				if seg32.Maxprot&Write == 0 {
   142  					buf := make([]byte, 4)
   143  					_, err := r.ReadAt(buf, offset+int64(unsafe.Offsetof(seg32.Maxprot)))
   144  					if err != nil {
   145  						return false, fmt.Errorf("failed to read MaxProt uint32: %w", err)
   146  					}
   147  					newPerms := bo.Uint32(buf) | Write
   148  					bo.PutUint32(buf, newPerms)
   149  					_, err = r.WriteAt(buf, offset+int64(unsafe.Offsetof(seg32.Maxprot)))
   150  					if err != nil {
   151  						return false, fmt.Errorf("failed to write MaxProt uint32: %w", err)
   152  					}
   153  					if seg32.Maxprot != newPerms {
   154  						havePatched = true
   155  					}
   156  				}
   157  				textIsWriteable = true
   158  			}
   159  		case macho.LoadCmdSegment64:
   160  			var seg64 macho.Segment64
   161  			b := bytes.NewReader(cmddat)
   162  			if err := binary.Read(b, bo, &seg64); err != nil {
   163  				return false, fmt.Errorf("failed to read LoadCmdSegment64: %w", err)
   164  			}
   165  			if cstring(seg64.Name[0:]) == "__TEXT" {
   166  				if seg64.Maxprot&Write == 0 {
   167  					buf := make([]byte, 4)
   168  					_, err := r.ReadAt(buf, offset+int64(unsafe.Offsetof(seg64.Maxprot)))
   169  					if err != nil {
   170  						return false, fmt.Errorf("failed to read MaxProt uint32: %w", err)
   171  					}
   172  					newPerms := bo.Uint32(buf) | Write
   173  					bo.PutUint32(buf, newPerms)
   174  					_, err = r.WriteAt(buf, offset+int64(unsafe.Offsetof(seg64.Maxprot)))
   175  					if err != nil {
   176  						return false, fmt.Errorf("failed to write MaxProt uint32: %w", err)
   177  					}
   178  					if seg64.Maxprot != newPerms {
   179  						havePatched = true
   180  					}
   181  				}
   182  				textIsWriteable = true
   183  			}
   184  		}
   185  
   186  		offset += int64(siz)
   187  	}
   188  	return havePatched, nil
   189  }
   190  
   191  func cstring(b []byte) string {
   192  	i := bytes.IndexByte(b, 0)
   193  	if i == -1 {
   194  		i = len(b)
   195  	}
   196  	return string(b[0:i])
   197  }