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 }