-
Notifications
You must be signed in to change notification settings - Fork 26
/
loader_darwin.go
391 lines (353 loc) · 11.1 KB
/
loader_darwin.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
package universal
import (
"bytes"
"errors"
"fmt"
"io/ioutil"
"unsafe"
"github.com/Binject/debug/macho"
"github.com/awgh/cppgo/asmcall/cdecl"
"github.com/awgh/rawreader"
"golang.org/x/sys/unix"
)
const MaxUint = ^uint(0)
const MaxInt = int(MaxUint >> 1)
const LIB_DYLD_PATH = "/usr/lib/system/libdyld.dylib"
func syscallSharedRegionCheckNp() (uint64, error) {
var address uint64
r1, _, _ := unix.RawSyscall(unix.SYS_SHARED_REGION_CHECK_NP, uintptr(unsafe.Pointer(&address)), 0, 0)
if r1 != 0 {
return 0, errors.New("shared region check failed")
}
return address, nil
}
func isSierra() bool {
var utsName unix.Utsname
err := unix.Uname(&utsName)
if err != nil {
return true
}
if utsName.Release[0] == '1' && utsName.Release[1] < '6' {
return false
}
if utsName.Release[0] == '9' && utsName.Release[1] == '.' {
return false
}
return true
}
// LoadLibraryImpl - loads a single library to memory, without trying to check or load required imports
func LoadLibraryImpl(name string, image *[]byte) (*Library, error) {
// On MacOS Sierra and up, NSLinkModule is just a wrapper around
// dlopen, pwrite and unlink. There's no "in-memory" only method using
// dyld4 APIs, so there's no point in using NSCreateObjectFileImageFromMemory and NSLinkModule anymore.
// We can just write our own version that:
// - writes the binary image to a temporary file
// - calls dlopen on the temporary file
// - unlinks the temporary file
// - uses dlsym to resolve all the symbols
// See https://github.com/apple-oss-distributions/dyld/blob/main/dyld/DyldAPIs.cpp#L2785-L2847 for more details.
if isSierra() {
return LoadLibraryDlopen(name, image)
}
// force input image to be a bundle
// this is not the most efficient method, but it does create the abililty to do more fixups down the road,
// like if we added universal file support
machoFile, err := macho.NewFile(bytes.NewReader(*image))
if err != nil {
return nil, err
}
machoFile.FileHeader.Type = macho.TypeBundle
newImage, err := machoFile.Bytes()
if err != nil {
return nil, err
}
image = &newImage
// Find dyld, loop through all the Mach-O images in memory until we find
// one that exports NSCreateObjectFileImageFromMemory and NSLinkModule
var execBase uintptr = uintptr(0x0000000001000000)
ptr := execBase
var createObjectAddr, linkModuleAddr uint64
for i := 0; i < 10; i++ { // quit after the first ten images
ptr, err = findMacho(ptr, 0x1000)
if err != nil {
return nil, err
}
fmt.Printf("Found image at 0x%08x\n", ptr)
rawr := rawreader.New(ptr, MaxInt)
machoFile, err := macho.NewFileFromMemory(rawr)
if err != nil {
return nil, err
}
exports := machoFile.Exports()
foundCreateObj := false
foundLinkModule := false
for _, export := range exports {
if export.Name == "_NSCreateObjectFileImageFromMemory" {
fmt.Println("found NSCreateObjectFileImageFromMemory")
createObjectAddr = export.VirtualAddress
foundCreateObj = true
}
if export.Name == "_NSLinkModule" {
fmt.Println("found NSLinkModule")
linkModuleAddr = export.VirtualAddress
foundLinkModule = true
}
}
if foundCreateObj && foundLinkModule {
fmt.Println("Found dyld!")
break
}
ptr = ptr + 0x1000
}
createObjectProc := ptr + uintptr(createObjectAddr)
linkModuleProc := ptr + uintptr(linkModuleAddr)
imageLen := uint64(len(*image))
var NSObjectFileImage uintptr
_, err = cdecl.Call(createObjectProc,
uintptr(unsafe.Pointer(&(*image)[0])), // pointer to image (*char)
uintptr(unsafe.Pointer(&imageLen)), // length of image (size_t)
uintptr(unsafe.Pointer(&NSObjectFileImage))) // output pointer to struct
if err != nil {
return nil, err
}
options := uint64(3) // NSLINKMODULE_OPTION_PRIVATE | NSLINKMODULE_OPTION_BINDNOW
modPtr, err := cdecl.Call(linkModuleProc,
uintptr(unsafe.Pointer(NSObjectFileImage)), // file image struct from NSCreateObjectFileImageFromMemory
uintptr(unsafe.Pointer(&name)), // image name
uintptr(unsafe.Pointer(&options))) // image options flags
if err != nil {
return nil, err
}
libPtr, err := findMachoPtr(modPtr)
if err != nil {
fmt.Println(err)
return nil, err
}
fmt.Printf("Found image at 0x%08x\n", libPtr)
// parse the Mach-o we just loaded to get the exports
rawr := rawreader.New(libPtr, MaxInt)
machoFile, err = macho.NewFileFromMemory(rawr)
if err != nil {
return nil, err
}
exports := machoFile.Exports()
if err != nil {
return nil, err
}
lib := Library{
BaseAddress: libPtr,
Exports: make(map[string]uint64),
}
for _, x := range exports {
lib.Exports[x.Name] = uint64(x.VirtualAddress)
}
return &lib, nil
}
// LoadLibraryDlopen - loads a single library using dlopen and resolves
// all exported symbols using dlsym. This function writes to a temporary
// file and then loads the file into memory, and unlinks it.
// This is similar to what NSLinkModule does nowadays.
func LoadLibraryDlopen(name string, image *[]byte) (*Library, error) {
var (
dlopenAddr uint64
dlSymAddr uint64
dyldAddress uint64
)
machoFile, err := macho.NewFile(bytes.NewReader(*image))
if err != nil {
return nil, err
}
// We use the shared library cache to find the libdyld.dylib wrapper loaded into our process.
// Once we have the address of the libdyld.dylib wrapper, we can parse it in memory using debug/macho (thanks Awgh)
// and find the exports we're interested in: dlopen and dlsym.
// This part of the code is a port from the awesome work of usiegl00: https://github.com/usiegl00/metasploit-framework/blob/osx-stage-monterey/external/source/shellcode/osx/stager/main.c#L159-L183
sharedRegionStart, err := syscallSharedRegionCheckNp()
if err != nil {
return nil, err
}
dyldCacheheader := (*DyldCacheHeader)(unsafe.Pointer(uintptr(sharedRegionStart)))
imagesCount := dyldCacheheader.ImagesCountOld
if imagesCount == 0 {
imagesCount = dyldCacheheader.ImagesCount
}
imagesOffset := dyldCacheheader.ImagesOffsetOld
if imagesOffset == 0 {
imagesOffset = dyldCacheheader.ImagesOffset
}
sharedFileMapping := (*SharedFileMapping)(unsafe.Pointer(uintptr(sharedRegionStart + uint64(dyldCacheheader.MappingOffset))))
dyldCacheImageInfo := (*DyldCacheImageInfo)(unsafe.Pointer(uintptr(sharedRegionStart + uint64(imagesOffset))))
for i := uint32(0); i < imagesCount; i++ {
pathFileOffset := sharedRegionStart + uint64(dyldCacheImageInfo.PathFileOffset)
dylibPath := readCString(pathFileOffset)
if dylibPath == LIB_DYLD_PATH {
dyldAddress = dyldCacheImageInfo.Address
break
}
dyldCacheImageInfo = (*DyldCacheImageInfo)(unsafe.Pointer(uintptr(sharedRegionStart+uint64(imagesOffset)) + uintptr(i)*unsafe.Sizeof(DyldCacheImageInfo{})))
}
offset := sharedRegionStart - sharedFileMapping.Address
dyldAddress += offset
rawr := rawreader.New(uintptr(dyldAddress), MaxInt)
dyldFile, err := macho.NewFileFromMemory(rawr)
if err != nil {
return nil, err
}
exports := dyldFile.Exports()
for _, export := range exports {
if export.Name == "_dlopen" {
dlopenAddr = export.VirtualAddress + offset
}
if export.Name == "_dlsym" {
dlSymAddr = export.VirtualAddress + offset
}
}
dlopenProc := uintptr(dlopenAddr)
dlSymProc := uintptr(dlSymAddr)
options := 0x80000080 // RTLD_UNLOADABLE | RTLD_NODELETE per https://github.dev/apple-oss-distributions/dyld/blob/419f8cbca6fb3420a248f158714a9d322af2aa5a/dyld/DyldAPIs.cpp#L2818-L2819
// Create a temp file to write the image to so we can dlopen it.
// I wish MacOS had a memfd_create equivalent ...
tmpFile, err := ioutil.TempFile("", "")
if err != nil {
return nil, err
}
filename := tmpFile.Name()
imagePath := append([]byte(filename), 0x00) // C strings are the worst
_, err = tmpFile.Write(*image)
if err != nil {
return nil, err
}
// We need to sign the written file otherwise dlopen will fail on MacOS 11 and up on Apple Silicon machines.
// We're just using ad-hoc code signing, so we're not going to do anything fancy here.
err = machoCodeSign(filename)
if err != nil {
return nil, err
}
pseudoHandle, err := cdecl.Call(dlopenProc,
uintptr(unsafe.Pointer(&imagePath[0])), // image name
uintptr(options)) // image options flags
if err != nil {
return nil, err
}
if pseudoHandle == 0 {
unix.Unlink(filename)
return nil, errors.New("dlopen returned 0")
}
// Unlink the temp file, we don't need it anymore
err = unix.Unlink(tmpFile.Name())
if err != nil {
return nil, err
}
exports = machoFile.Exports()
if err != nil {
return nil, err
}
lib := Library{
BaseAddress: pseudoHandle, // library pseudo handle, do not use directly
Exports: make(map[string]uint64),
}
// Since the return value from dlopen is a pseudo handle
// and not the address of the dylib in memory, we can't
// really use it to resolve symbols directly. Instead, we
// use dlsym to resolve all symbols ahead of time, that way
// the end user can just use Library.Call.
// The downside is that Library.BaseAddress is unusable and
// any attempt to use it will result in a crash.
for _, x := range exports {
exportName := append([]byte(x.Name[1:]), 0x00) // strip the _
exportPtr, err := cdecl.Call(dlSymProc,
pseudoHandle,
uintptr(unsafe.Pointer(&exportName[0])))
if err != nil {
return nil, err
}
lib.Exports[x.Name] = uint64(exportPtr)
}
return &lib, nil
}
func readCString(pathFileOffset uint64) string {
path := ""
pathSize := 0
for {
b := (*byte)(unsafe.Pointer(uintptr(pathFileOffset + uint64(pathSize))))
if *b == 0 {
break
}
path += string(*b)
pathSize++
}
return path
}
func isPtrValid(p uintptr) bool {
r1, _, err := unix.RawSyscall(unix.SYS_CHMOD, p, 0777, 0)
if r1 != 0 && err != 2 {
return false
}
return err == 2
}
func findMacho(addr uintptr, increment uint32) (uintptr, error) {
var (
base uintptr
header uint32
err error = fmt.Errorf("could not find macho")
)
ptr := addr
for {
if isPtrValid(ptr) {
header = *(*uint32)(unsafe.Pointer(ptr))
if header == macho.Magic64 {
base = ptr
err = nil
break
}
}
ptr += uintptr(increment)
}
return base, err
}
func findMachoPtr(addr uintptr) (uintptr, error) {
var (
base uintptr
header uint32
err error = fmt.Errorf("could not find macho")
)
idx := addr
ptr := uintptr(*(*uint64)(unsafe.Pointer(idx)))
for {
if isPtrValid(ptr) {
header = *(*uint32)(unsafe.Pointer(ptr))
if header == macho.Magic64 {
base = ptr
err = nil
break
}
}
idx += 8
ptr = uintptr(*(*uint64)(unsafe.Pointer(idx)))
}
return base, err
}
// Call - call a function in a loaded library
func (l *Library) Call(functionName string, args ...uintptr) (uintptr, error) {
var (
proc uintptr
funcPtr uint64
ok bool
)
if len(functionName) > 0 && functionName[0] != '_' {
functionName = "_" + functionName
} // OSX has underscore-prefixed exports
if !isSierra() {
proc, ok = l.FindProc(functionName)
} else {
// Can't rely on FindProc for Sierra and up since
// the BaseAddress is a pseudo handle to the lib,
// not the actual address.
funcPtr, ok = l.Exports[functionName]
proc = uintptr(funcPtr)
}
if !ok {
return 0, errors.New("Call did not find export " + functionName)
}
val, err := cdecl.Call(proc, args...)
return val, err
}