├── .gitignore ├── README.md ├── go.mod ├── go.sum ├── main.go ├── shell_memory.c └── shell_memory.h /.gitignore: -------------------------------------------------------------------------------- 1 | macos_shell_memory 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Execute Thin Mach-O Binaries in Memory 2 | 3 | This is a CGo implementation of the initial technique put forward by [Stephanie Archibald](https://twitter.com/@ParchedMind) in her blog, [Running Executables on macOS From Memory](https://blogs.blackberry.com/en/2017/02/running-executables-on-macos-from-memory). 4 | 5 | ## Usage 6 | 7 | ``` 8 | ./macos_shell_memory [bin] [args] 9 | ``` 10 | 11 | ## Description 12 | 13 | Given that `[bin]` is in `$PATH`, `[bin]` is loaded into memory and executed with `[args]` (if provided). Stdout and Stderr will be redirected during binary execution. Normally, when a Mach-O binary finishes execution, the program exits and returns back to the caller (like your terminal); however, this exit call, when called from your current process, will exit your loading process. 14 | 15 | To disable this functionality, a new `atexit` routine is registered to rewind stack-state back to before the in-memory Mach-O `main()` function ever executed. Doing so causes instability, and as such, we call `C._Exit` over letting the Go program exit normally. 16 | 17 | This weaponization could be modified to point to any thin Mach-O binary, and enforcing the `[bin]` to be in `$PATH` is an arbitrary constraint I've added. 18 | 19 | ## Important Caveats 20 | 21 | This works _only_ for thin Mach-O binaries. This can be seen by issuing the following command: 22 | 23 | ``` 24 | codesign -vvvv -d /path/to/bin 25 | ``` 26 | 27 | For example, `codesign -vvvv -d /bin/ps` returns: 28 | ``` 29 | Executable=/bin/ps 30 | Identifier=com.apple.ps 31 | Format=Mach-O thin (x86_64) 32 | ... snip ... 33 | ``` 34 | 35 | There are certain nuances that I haven't worked out for fat and ARM binaries. Doing so will cause the program to irrecoverably segfault. 36 | 37 | ## Examples 38 | 39 | ``` 40 | ╭─djh@bifrost ~/go/src/github.com/djhohnstein/macos_shell_memory ‹main*› 41 | ╰─$ ./macos_shell_memory ps [21/05/20 |12:18PM] 42 | [Go Code] Redirecting STDOUT... 43 | [Go Code] Successfully recovered from bin exit(), captured the following output: 44 | 45 | PID TTY TIME CMD 46 | 72116 ttys000 0:00.00 zsh 47 | 47918 ttys003 0:00.00 zsh 48 | 78749 ttys003 0:00.01 ./macos_shell_memory ps 49 | 612 ttys004 0:00.00 zsh 50 | 51 | ╭─djh@bifrost ~/go/src/github.com/djhohnstein/macos_shell_memory ‹main*› 52 | ╰─$ ./macos_shell_memory ls -alht [21/05/20 |12:18PM] 53 | [Go Code] Redirecting STDOUT... 54 | [Go Code] Successfully recovered from bin exit(), captured the following output: 55 | 56 | total 4752 57 | drwxr-xr-x 13 djh staff 416B May 20 12:18 .git 58 | -rwxr-xr-x 1 djh staff 2.3M May 20 12:16 macos_shell_memory 59 | drwxr-xr-x 11 djh staff 352B May 20 11:50 . 60 | -rw-r--r-- 1 djh staff 0B May 20 11:50 README.md 61 | -rw-r--r-- 1 djh staff 3.1K May 20 11:30 main.go 62 | -rw-r--r-- 1 djh staff 3.9K May 20 11:23 shell_memory.c 63 | drwxr-xr-x 5 djh staff 160B May 18 16:04 .. 64 | -rw-r--r-- 1 djh staff 883B May 17 16:43 go.sum 65 | -rw-r--r-- 1 djh staff 253B May 17 16:43 go.mod 66 | -rw-r--r-- 1 djh staff 19B May 16 17:15 .gitignore 67 | -rw-r--r-- 1 djh staff 143B May 16 17:09 shell_memory.h 68 | 69 | ``` 70 | 71 | ## References 72 | - https://blogs.blackberry.com/en/2017/02/running-executables-on-macos-from-memory 73 | - https://github.com/MythicAgents/poseidon/blob/master/Payload_Type/poseidon/agent_code/execute_memory/execute_memory_darwin.m 74 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/djhohnstein/macos_shell_memory 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/fengyoulin/hookingo v0.1.1 // indirect 7 | golang.org/x/arch v0.0.0-20210502124803-cbf565b21d1e // indirect 8 | golang.org/x/sys v0.0.0-20210514084401-e8d321eab015 // indirect 9 | ) 10 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/fengyoulin/hookingo v0.1.1 h1:8Bq1OUtkjLA6TV+G+JW0uxSJi5Tqw5GbGwnXV3NL3Qg= 2 | github.com/fengyoulin/hookingo v0.1.1/go.mod h1:qXWbi6b1dIXCyvCyk7Nbnv4ZWO3wfMp8Ih/etuOkfl0= 3 | golang.org/x/arch v0.0.0-20190927153633-4e8777c89be4/go.mod h1:flIaEI6LNU6xOCD5PaJvn9wGP0agmIOqjrtsKGRguv4= 4 | golang.org/x/arch v0.0.0-20210502124803-cbf565b21d1e h1:pv3V0NlNSh5Q6AX/StwGLBjcLS7UN4m4Gq+V+uSecqM= 5 | golang.org/x/arch v0.0.0-20210502124803-cbf565b21d1e/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= 6 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 7 | golang.org/x/sys v0.0.0-20210514084401-e8d321eab015 h1:hZR0X1kPW+nwyJ9xRxqZk1vx5RUObAPBdKVvXPDUH/E= 8 | golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 9 | rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= 10 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | //#cgo LDFLAGS: -lm -framework Foundation 4 | //#cgo CFLAGS: -Wno-error=implicit-function-declaration -Wno-deprecated-declarations -Wno-format -Wno-int-conversion 5 | //#include 6 | //#include 7 | //#include "shell_memory.h" 8 | import "C" 9 | import "os" 10 | import "os/exec" 11 | import "unsafe" 12 | import "io/ioutil" 13 | import "io" 14 | import "bytes" 15 | import "fmt" 16 | import "syscall" 17 | import "log" 18 | // import "github.com/djhohnstein/macos_shell_memory/stdouterr" 19 | 20 | 21 | func main() { 22 | if len(os.Args) < 2 { 23 | panic("Require at least one argument to execute.") 24 | } 25 | argv := os.Args[1:] 26 | path, err := exec.LookPath(argv[0]) 27 | if err != nil { 28 | panic(fmt.Sprintf("%s is not installed in $PATH", argv[0])) 29 | } 30 | argv[0] = path 31 | fileBytes, err := ioutil.ReadFile(path) 32 | if err != nil { 33 | panic(fmt.Sprintf("could not read file %s. Reason: %s", path, err.Error())) 34 | } 35 | // Create our C-esque arguments 36 | c_argc := C.int(len(argv)) 37 | c_argv := C.allocArgv(c_argc) 38 | defer C.free(c_argv) 39 | 40 | // Convert each argv to a char* 41 | for i, arg := range argv { 42 | tmp := C.CString(arg) 43 | defer C.free(unsafe.Pointer(tmp)) 44 | C.addArg(c_argv, tmp, C.int(i)) 45 | } 46 | cBytes := C.CBytes(fileBytes) 47 | defer C.free(cBytes) 48 | cLenBytes := C.int(len(fileBytes)) 49 | 50 | 51 | // Redirect STD handles to pipes... 52 | fmt.Println("[Go Code] Redirecting STDOUT..."); 53 | 54 | // Clone Stdout to origStdout. 55 | origStdout, err := syscall.Dup(syscall.Stdout) 56 | if err != nil { 57 | log.Fatal(err) 58 | } 59 | // Clone Stdout to origStdout. 60 | origStderr, err := syscall.Dup(syscall.Stderr) 61 | if err != nil { 62 | log.Fatal(err) 63 | } 64 | 65 | rStdout, wStdout, err := os.Pipe() 66 | if err != nil { 67 | log.Fatal(err) 68 | } 69 | 70 | rStderr, wStderr, err := os.Pipe() 71 | if err != nil { 72 | log.Fatal(err) 73 | } 74 | 75 | 76 | reader := io.MultiReader(rStdout, rStderr) 77 | 78 | // Clone the pipe's writer to the actual Stdout descriptor; from this point 79 | // on, writes to Stdout will go to w. 80 | if err = syscall.Dup2(int(wStderr.Fd()), syscall.Stdout); err != nil { 81 | log.Fatal(err) 82 | } 83 | 84 | // Clone the pipe's writer to the actual Stderr descriptor; from this point 85 | // on, writes to Stderr will go to w. 86 | if err = syscall.Dup2(int(wStderr.Fd()), syscall.Stderr); err != nil { 87 | log.Fatal(err) 88 | } 89 | 90 | // Background goroutine that drains the reading end of the pipe. 91 | out := make(chan []byte) 92 | go func() { 93 | var b bytes.Buffer 94 | io.Copy(&b, reader) 95 | out <- b.Bytes() 96 | }() 97 | 98 | // END redirect 99 | 100 | C.execMachO((*C.char)(cBytes), cLenBytes, c_argc, c_argv) 101 | 102 | // BEGIN redirect 103 | 104 | C.fflush(nil) 105 | wStdout.Close() 106 | wStderr.Close() 107 | syscall.Close(syscall.Stdout) 108 | syscall.Close(syscall.Stderr) 109 | // Rendezvous with the reading goroutine. 110 | b := <-out 111 | 112 | // Restore original Stdout and Stderr. 113 | syscall.Dup2(origStdout, syscall.Stdout) 114 | syscall.Dup2(origStderr, syscall.Stderr) 115 | syscall.Close(origStdout) 116 | syscall.Close(origStderr) 117 | fmt.Println("[Go Code] Successfully recovered from bin exit(), captured the following output:\n\n", string(b)) 118 | 119 | 120 | // END redirect 121 | C._Exit(0) 122 | } 123 | -------------------------------------------------------------------------------- /shell_memory.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | #include 16 | #include 17 | #include 18 | 19 | #include "shell_memory.h" 20 | 21 | #define SIGTERM_MSG "SIGTERM received.\n" 22 | 23 | 24 | // Stack info for when dynamically loaded program exits 25 | static jmp_buf SAVED_ENV; 26 | // Integer switch for program control flow on setjmp 27 | static int RETVAL = 0; 28 | 29 | void my_exit() { 30 | if (RETVAL == 0) { 31 | longjmp(SAVED_ENV, 1); 32 | } else { 33 | return; 34 | } 35 | } 36 | 37 | // Allocate a new char** pointer to hold new arguments 38 | void* allocArgv(int argc) { 39 | char** argv = malloc(sizeof(char *) * argc + 1); 40 | argv[argc] = NULL; 41 | return (void*)argv; 42 | } 43 | 44 | // Stuff arguments into the char** pointer as doing this 45 | // strictly in Go sucks. 46 | void addArg(void* argv, char* arg, int i) { 47 | ((char**)argv)[i] = arg; 48 | } 49 | 50 | // Find the entry point command by searching through base's load commands. 51 | // This will give us the offset required to execute the MachO 52 | int find_epc(unsigned long base, struct entry_point_command **entry) { 53 | struct mach_header_64 *mh; 54 | struct load_command *lc; 55 | 56 | unsigned long text = 0; 57 | 58 | *entry = NULL; 59 | 60 | mh = (struct mach_header_64 *)base; 61 | lc = (struct load_command *)(base + sizeof(struct mach_header_64)); 62 | for(int i=0; incmds; i++) { 63 | if(lc->cmd == LC_MAIN) { //0x80000028 64 | *entry = (struct entry_point_command *)lc; 65 | return 0; 66 | } 67 | 68 | lc = (struct load_command *)((unsigned long)lc + lc->cmdsize); 69 | } 70 | 71 | return 1; 72 | } 73 | 74 | // Executes a MachO (given by fileBytes) with requisite arguments. 75 | int execMachO(char* fileBytes, int szFile, int argc, void* argv) { 76 | NSObjectFileImage fileImage = NULL; 77 | NSModule module = NULL; 78 | NSSymbol symbol = NULL; 79 | void* pSymbolAddress = NULL; 80 | RETVAL = 0; 81 | int(*main)(int, char**, char**, char**); 82 | 83 | 84 | 85 | int type = ((int *)fileBytes)[3]; 86 | if(type != 0x8) ((int *)fileBytes)[3] = 0x8; //change to mh_bundle type 87 | 88 | // Mapping the image into memory 89 | NSCreateObjectFileImageFromMemory(fileBytes, szFile, &fileImage); 90 | 91 | if(fileImage == NULL){ 92 | return -1; 93 | } 94 | module = NSLinkModule(fileImage, "module", NSLINKMODULE_OPTION_PRIVATE | 95 | NSLINKMODULE_OPTION_BINDNOW); 96 | 97 | 98 | // Find the __mh_execute_header 99 | symbol = NSLookupSymbolInModule(module, "__mh_execute_header"); 100 | 101 | if(type == 0x2) { //mh_execute 102 | struct entry_point_command *epc; 103 | pSymbolAddress = NSAddressOfSymbol(symbol); 104 | // Get entrypoint 105 | if(find_epc(pSymbolAddress, &epc)) { 106 | fprintf(stderr, "Could not find ec.\n"); 107 | goto err; 108 | } 109 | // Save callstack. On first call, setjmp returns 0. 110 | // On longjmp, setjmp returns whatever longjmp specifies. 111 | // In this case, we say "anything other than 0, execute MachO" 112 | RETVAL = setjmp(SAVED_ENV); 113 | if (RETVAL == 0) { 114 | // Create an atexit routine to longjmp back to our saved buffer. 115 | // When the thin MachO executes in-memory, it'll attempt to exit 116 | // the program. Creating this thin hook allows us to stop that process. 117 | atexit(my_exit); 118 | 119 | // Calcuate the true address of the main() entry 120 | unsigned long tmp = pSymbolAddress + epc->entryoff; 121 | main = (int(*)(int, char**, char**, char**)) (tmp); 122 | 123 | if(main == NULL){ 124 | printf("Failed to find address of main\n"); 125 | } 126 | 127 | // Invoking a MachO's main() function will induce an uncatchable SIGKILL 128 | // which means any code after this line will not be executed. 129 | main(argc, (char**)argv, NULL, NULL); 130 | } 131 | // cleanup 132 | NSUnLinkModule(module, NSLINKMODULE_OPTION_PRIVATE | NSLINKMODULE_OPTION_BINDNOW); 133 | NSDestroyObjectFileImage(fileImage); 134 | return 0; 135 | } 136 | err: 137 | // cleanup 138 | if (module != NULL) { 139 | NSUnLinkModule(module, NSLINKMODULE_OPTION_PRIVATE | NSLINKMODULE_OPTION_BINDNOW); 140 | } 141 | if (fileImage != NULL) { 142 | NSDestroyObjectFileImage(fileImage); 143 | } 144 | return -1; 145 | } 146 | -------------------------------------------------------------------------------- /shell_memory.h: -------------------------------------------------------------------------------- 1 | #ifndef SHELL_MEMORY 2 | #define SHELL_MEMORY 3 | 4 | void* allocArgv(int); 5 | void addArg(void*, char*, int); 6 | int execMachO(char*, int, int, void*); 7 | #endif 8 | --------------------------------------------------------------------------------