├── .gitignore ├── .gitmodules ├── .travis.yml ├── LICENSE ├── README.md ├── Rakefile ├── task_vaccine.c ├── task_vaccine.h └── tests ├── Rakefile ├── demo_payload.c ├── demo_target.c └── tests.c /.gitignore: -------------------------------------------------------------------------------- 1 | # Build artifacts 2 | # 3 | build/ 4 | build_tests/ 5 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "submodules/liblorgnette"] 2 | path = submodules/liblorgnette 3 | url = https://github.com/rodionovd/liblorgnette.git 4 | [submodule "submodules/Cegta"] 5 | path = submodules/Cegta 6 | url = https://github.com/rodionovd/Cegta.git 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: objective-c 2 | osx_image: xcode61 3 | before_script: cd ./tests 4 | script: rake 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Dmitry Rodionov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## task_vaccine 2 | 3 | [![Build Status](https://travis-ci.org/rodionovd/task_vaccine.svg?branch=master)](https://travis-ci.org/rodionovd/task_vaccine) ![GitHub version](https://badge.fury.io/gh/rodionovd%2Ftask_vaccine.svg) 4 | 5 | Yet another code injection library for OS X. 6 | 7 | 8 | #### TL;DR 9 | 10 | ```sh 11 | $ git clone --recursive https://github.com/rodionovd/task_vaccine.git task_vaccine 12 | $ cd ./task_vaccine 13 | $ rake test 14 | $ rake build # will build an x86_64 dynamic library and place it into ./build/x86_64 15 | ``` 16 | 17 | ```c 18 | #include "task_vaccine.h" 19 | 20 | task_t target = ...; 21 | int err = task_vaccine(target, "./payload0.dylib"); 22 | if (err != KERN_SUCCESS) { 23 | fprintf(stderr, "task_vaccine() failed with error: %d\n", err); 24 | } 25 | ``` 26 | see [Usage](#usage) for details. 27 | 28 | 29 | #### Why should I use this thing instead of [`mach_inject`](https://github.com/rentzsch/mach_inject)? 30 | Well, for a couple of reasons actually: 31 | 32 | 1. `mach_inject`'s codebase is old and it hasn't been updated for a while. 33 | 2. You **can not** inject `i386` targets from `x86_64` hosts and vice versa using `mach_inject`, so you should use two different injectors. *With `task_vaccine` you* **can** *actually do it.* 34 | 3. I have [automated tests](./tests/Rakefile) 🚦 35 | 36 | #### How it works 37 | 38 | Pretty straightforward, see: 39 | 40 | 1. At first, we create a new thread inside a target task (process) and execute `_pthread_set_self()` function on it. 41 | 42 | > We can only create a raw Mach thread inside a target task. But many functions (such as `dlopen()`) rely on pthread stuff (locks, etc), so we have to initialize a pthread first and only then execute `dlopen()` for loading the payload. 43 | 44 | 2. Then, `_pthread_set_self()` returns into an invalid memory address throwing an `EXC_BAD_ACCESS` exception. 45 | 3. We catch this exception and reconfigure the thread to launch `dlopen()` with a given library path. When it returns, one more `EXC_BAD_ACCESS` exception is thrown — we catch 'em as well and terminate the remote thread. 46 | 4. We examine a `dlopen()` return value then: if it's greater than zero, `task_vaccine` succeeded. 47 | 48 | #### Caveats 49 | As you may have notice `task_vaccine()` takes a `task_t` argument. This means you should obtain a task port of your target first: 50 | 51 | ```c 52 | pid_t proc = ...; 53 | task_t task; 54 | task_for_pid(mach_task_self(), proc, &task); 55 | ``` 56 | 57 | **Of course, you must have an ability to control other processes (i.e. you should be a member of `procmod` user group. Being root is also OK).** 58 | 59 | #### Usage 60 | 61 | A prototype for the function is the following: 62 | 63 | ```c 64 | kern_return_t task_vaccine(task_t target, const char *payload_path); 65 | ``` 66 | 67 | | Parameter | Type (in/out) | Description | 68 | | :--------: | :-----------: | :---------- | 69 | | `target` | in | _**(required)**_ An identifier of the target process | 70 | | `payload_path ` | in| _**(required)**_ A full or relative path to the library you want to load into the given target. The library should be compatible with the target task. In order to be meaningful, this library should also contain a constructor function that will be executed on load | 71 | 72 | 73 | | Return value | Description | 74 | | :---------- | :---------- | 75 | | KERN_SUCCESS | The payload was loaded successfully into the target task | 76 | | KERN_INVALID_TASK | `dlopen()` failed to load the given library (e.g. target architecture doesn't match the payload) | 77 | | KERN_FAILURE | Something gone totally wrong on a `task_vaccine`’s side. Please [open an issue](https://github.com/rodionovd/task_vaccine/issues/new) | 78 | 79 | --------- 80 | 81 | If you found any bug(s) or something, please open an issue or a pull request — I'd appreciate your help! (^,,^) 82 | 83 | Dmitry Rodionov, 2014 84 | i.am.rodionovd@gmail.com 85 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | task :default => [:build, :exit] 2 | 3 | SOURCES = "task_vaccine.c submodules/liblorgnette/lorgnette.c" 4 | CC_FLAGS = "" 5 | COMPILER = "clang" 6 | $status = 0 7 | # Tests subrake's namespace 8 | TESTS_NAMESPACE = "task_vaccine_tests" 9 | import "#{Dir.pwd}/tests/Rakefile" 10 | 11 | desc "Initialize and update submodules" 12 | task :bootstrap do 13 | system("git submodule update --init"); 14 | end 15 | 16 | desc "Clean up the environment" 17 | task :clear do 18 | system("rm -Rf ./build") 19 | end 20 | 21 | desc "Finish" 22 | task :exit do 23 | exit $status 24 | end 25 | 26 | def build_library(arches, dynamic) 27 | arch_string = "-arch " + arches.join(" -arch ") 28 | dir = "build" 29 | if (arches.count == 1) then 30 | dir = "build/#{arches[0]}" 31 | else 32 | dir = "build/fat" 33 | end 34 | system("mkdir -p ./#{dir}") if (!File.exists?(Dir.getwd + "/#{dir}")) 35 | 36 | if (dynamic) then 37 | system("#{COMPILER} #{arch_string} \ 38 | -dynamiclib -single_module \ 39 | -o ./#{dir}/task_vaccine.dylib \ 40 | #{SOURCES}") 41 | else 42 | system("#{COMPILER} #{arch_string} -c #{SOURCES}") 43 | system("libtool -static -o ./#{dir}/task_vaccine.a *.o") 44 | system("rm *.o") 45 | end 46 | 47 | if ($status == 0) then 48 | $status = $?.exitstatus 49 | end 50 | end 51 | 52 | desc "Build everything" 53 | task :all do 54 | Rake::Task["clear"].execute 55 | build_library(["x86_64"], true) 56 | build_library(["x86_64"], false) 57 | build_library(["i386"], true) 58 | build_library(["i386"], false) 59 | build_library(["i386", "x86_64"], true) 60 | build_library(["i386", "x86_64"], false) 61 | end 62 | 63 | # Testing 64 | 65 | desc "Test the library" 66 | task :test do 67 | Rake::Task["#{TESTS_NAMESPACE}:default"].invoke 68 | end 69 | 70 | # Dynamic library target 71 | 72 | desc "General build" 73 | task :build do 74 | build_library(["x86_64"], true) 75 | end 76 | 77 | desc "Build for x86_64" 78 | task :build_64 do 79 | build_library(["x86_64"], true) 80 | end 81 | 82 | desc "Build for i386" 83 | task :build_32 do 84 | build_library(["i386"], true) 85 | end 86 | 87 | desc "Build for x86_64 and i386" 88 | task :build_fat do 89 | build_library(["i386", "x86_64"], true) 90 | end 91 | 92 | # Static library target 93 | 94 | desc "General static build" 95 | task :static do 96 | build_library(["x86_64"], false) 97 | end 98 | 99 | desc "Build static for x86_64" 100 | task :static_64 do 101 | build_library(["x86_64"], false) 102 | end 103 | 104 | desc "Build static for i386" 105 | task :static_32 do 106 | build_library(["i386"], false) 107 | end 108 | 109 | desc "Build static for x86_64 and i386" 110 | task :static_fat do 111 | build_library(["i386", "x86_64"], false) 112 | end 113 | -------------------------------------------------------------------------------- /task_vaccine.c: -------------------------------------------------------------------------------- 1 | // task_vaccine.c 2 | // Copyright (c) 2014 Dmitry Rodionov 3 | // https://github.com/rodionovd/task_vaccine 4 | // 5 | // This software may be modified and distributed under the terms 6 | // of the MIT license. See the LICENSE file for details. 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | #include "task_vaccine.h" 15 | #include "submodules/liblorgnette/lorgnette.h" 16 | 17 | #define kVaccineRemoteStackSize (30*1024) 18 | #define kVaccineJumpToDlopenReturnValue (0xabad1dea) 19 | #define kVaccineFinishReturnValue (0xdead1dea) 20 | 21 | #define kVaccineErrorCode (-2) 22 | #define VaccineFailOnError(func) \ 23 | do { \ 24 | if (err != KERN_SUCCESS) { \ 25 | syslog(LOG_NOTICE, "[%d] "#func"() failed: %d (%s)\n", __LINE__-3, \ 26 | err, mach_error_string(err)); \ 27 | return kVaccineErrorCode; \ 28 | } \ 29 | } while (0) 30 | 31 | // From xnu-2782.1.97/bsd/uxkern/ux_exception.c 32 | typedef struct { 33 | mach_msg_header_t Head; 34 | /* start of the kernel processed data */ 35 | mach_msg_body_t msgh_body; 36 | mach_msg_port_descriptor_t thread; 37 | mach_msg_port_descriptor_t task; 38 | /* end of the kernel processed data */ 39 | NDR_record_t NDR; 40 | exception_type_t exception; 41 | mach_msg_type_number_t codeCnt; 42 | mach_exception_data_t code; 43 | /* some times RCV_TO_LARGE probs */ 44 | char pad[512]; 45 | } exc_msg_t; 46 | 47 | /** 48 | * 49 | */ 50 | static 51 | thread_state_flavor_t task_thread_flavor(task_t target) 52 | { 53 | assert(target); 54 | 55 | pid_t pid; 56 | int err = pid_for_task(target, &pid); 57 | if (err != KERN_SUCCESS) return (-1); 58 | 59 | int mib[4] = { 60 | CTL_KERN, KERN_PROC, KERN_PROC_PID, pid 61 | }; 62 | struct kinfo_proc info; 63 | size_t size = sizeof(info); 64 | 65 | err = sysctl(mib, 4, &info, &size, NULL, 0); 66 | if (err != KERN_SUCCESS) { 67 | return (-1); 68 | } 69 | 70 | if (info.kp_proc.p_flag & P_LP64) { 71 | return x86_THREAD_STATE64; 72 | } else { 73 | return x86_THREAD_STATE32; 74 | } 75 | } 76 | 77 | /** 78 | * 79 | */ 80 | static 81 | thread_act_t vaccine_thread32(task_t target, mach_vm_address_t stack, 82 | mach_vm_address_t dlopen_arg) 83 | { 84 | assert(target), assert(stack), assert(dlopen_arg); 85 | 86 | stack += kVaccineRemoteStackSize/2; 87 | // Allocate some place for a dummy pthread struct 88 | mach_vm_address_t dummy = 0; 89 | int err = mach_vm_allocate(target, &dummy, sizeof(struct _opaque_pthread_t), 90 | VM_FLAGS_ANYWHERE); 91 | VaccineFailOnError(mach_vm_allocate); 92 | // Place a fake return address and this dummy struct into the stack 93 | uint32_t local_stack[] = { 94 | kVaccineJumpToDlopenReturnValue, (uint32_t)dummy 95 | }; 96 | size_t local_stack_size = sizeof(local_stack); 97 | err = mach_vm_write(target, stack, 98 | (vm_offset_t)local_stack, local_stack_size); 99 | VaccineFailOnError(mach_vm_write); 100 | // Initialize an i386 thread state 101 | x86_thread_state32_t state; 102 | memset(&state, 0, sizeof(state)); 103 | // We can only create a raw Mach thread inside a target task. But many 104 | // functions (such as dlopen()) rely on pthread stuff (locks, etc), so we 105 | // have to initialize a pthread first and only then execute dlopen() for 106 | // loading the payload. 107 | // 108 | // As for OS X 10.9/10.10 there're two system libraries containing 109 | // _pthread_set_self symbol: 110 | // (1) libsystem_kernel.dylib and (2) libsystem_pthread.dylib. 111 | // In the former one is a no-op function (not exported actually) while the 112 | // latter holds the real (exported) symbol. 113 | // Since liblorgnette doesn't care about symbols publicity we have to 114 | // explicitly specify the source image for a symbol here. Otherwise it can 115 | // return an address of the no-op variant. 116 | uint32_t entrypoint = lorgnette_lookup_image(target, "_pthread_set_self", 117 | "libsystem_pthread.dylib"); 118 | if (entrypoint == 0) err = KERN_FAILURE; 119 | VaccineFailOnError(entrypoint); 120 | state.__eip = entrypoint; 121 | state.__esp = stack; 122 | state.__ebx = dlopen_arg; 123 | 124 | thread_act_t thread; 125 | err = thread_create(target, &thread); 126 | VaccineFailOnError(thread_create); 127 | err = thread_set_state(thread, x86_THREAD_STATE32, (thread_state_t)&state, 128 | x86_THREAD_STATE32_COUNT); 129 | VaccineFailOnError(thread_set_state); 130 | 131 | return thread; 132 | } 133 | 134 | /** 135 | * 136 | */ 137 | static 138 | thread_act_t vaccine_thread64(task_t target, mach_vm_address_t stack, 139 | mach_vm_address_t dlopen_arg) 140 | { 141 | assert(target), assert(stack), assert(dlopen_arg); 142 | 143 | stack += kVaccineRemoteStackSize; 144 | // Allocate some place for a dummy pthread struct 145 | mach_vm_address_t dummy = 0; 146 | int err = mach_vm_allocate(target, &dummy, sizeof(struct _opaque_pthread_t), 147 | VM_FLAGS_ANYWHERE); 148 | VaccineFailOnError(mach_vm_allocate); 149 | // Place a fake return address onto the stack 150 | uint64_t local_stack[] = { 151 | kVaccineJumpToDlopenReturnValue 152 | }; 153 | size_t local_stack_size = sizeof(local_stack); 154 | err = mach_vm_write(target, (stack - local_stack_size), 155 | (vm_offset_t)local_stack, local_stack_size); 156 | VaccineFailOnError(mach_vm_write); 157 | // Iinitilize an x86_64 thread state 158 | x86_thread_state64_t state; 159 | memset(&state, 0, sizeof(state)); 160 | // See a comment in vaccine_thread32() about why we refer to some strange 161 | // pthread thing instead of dlopen() here, and also why we need to explicitly 162 | // specify the source image name here 163 | uint64_t entrypoint = lorgnette_lookup_image(target, "_pthread_set_self", 164 | "libsystem_pthread.dylib"); 165 | if (entrypoint == 0) err = KERN_FAILURE; 166 | VaccineFailOnError(entrypoint); 167 | state.__rip = entrypoint; 168 | // (RDI) <- dummy pthread struct 169 | state.__rdi = dummy; 170 | // we simulate a ret instruction here, so descrease the stack 171 | state.__rsp = stack - 8; 172 | state.__rbx = dlopen_arg; 173 | 174 | thread_act_t thread; 175 | err = thread_create(target, &thread); 176 | VaccineFailOnError(thread_create); 177 | err = thread_set_state(thread, x86_THREAD_STATE64, (thread_state_t)&state, 178 | x86_THREAD_STATE64_COUNT); 179 | VaccineFailOnError(thread_set_state); 180 | 181 | return thread; 182 | } 183 | 184 | /** 185 | * Waits for a single exception on |exception_port| and returns. 186 | * For return value see mach_msg_server_once(). 187 | */ 188 | static 189 | kern_return_t vaccine_catch_exception(mach_port_t exception_port) 190 | { 191 | assert(exception_port); 192 | 193 | extern boolean_t exc_server(mach_msg_header_t *request, 194 | mach_msg_header_t *reply); 195 | kern_return_t err = mach_msg_server_once(exc_server, sizeof(exc_msg_t), 196 | exception_port, 0); 197 | return err; 198 | } 199 | 200 | /** 201 | * Verifies that the given |thread| was suspended. 202 | * Writes (1) into |status| if it was, (0) otherwise. 203 | * 204 | * @return a positive number if thread_info() fails for |thread| 205 | * @return KERN_SUCCESS otherwise 206 | */ 207 | static 208 | kern_return_t thread_was_suspended(thread_act_t thread, int *status) 209 | { 210 | assert(thread), assert(status); 211 | 212 | thread_basic_info_data_t basic_info; 213 | mach_msg_type_number_t info_count = THREAD_BASIC_INFO_COUNT; 214 | int err = thread_info(thread, THREAD_BASIC_INFO, (thread_info_t)&basic_info, 215 | &info_count); 216 | if (err != KERN_SUCCESS) return err; 217 | 218 | *status = (basic_info.suspend_count > 0); 219 | return KERN_SUCCESS; 220 | } 221 | 222 | /** 223 | * Fetches a value of |EAX| (or |RAX|) register from the given |thread|'s state. 224 | * It's flavor is defined by |flavor| argument: it should be either 225 | * x86_THREAD_STATE32 or x86_THREAD_STATE64. 226 | */ 227 | static 228 | int64_t thread_get_ax_register(thread_act_t thread, thread_state_flavor_t flavor) 229 | { 230 | assert(thread), assert(flavor); 231 | 232 | int64_t ax = 0; 233 | int err = KERN_FAILURE; 234 | 235 | if (flavor == x86_THREAD_STATE32) { 236 | x86_thread_state32_t state; 237 | mach_msg_type_number_t count = x86_THREAD_STATE32_COUNT; 238 | err = thread_get_state(thread, x86_THREAD_STATE32, 239 | (thread_state_t)&state, &count); 240 | VaccineFailOnError(thread_get_state); 241 | int32_t tmp = (int32_t)state.__eax; 242 | ax = tmp; 243 | } else { 244 | x86_thread_state64_t state; 245 | mach_msg_type_number_t count = x86_THREAD_STATE64_COUNT; 246 | err = thread_get_state(thread, x86_THREAD_STATE64, 247 | (thread_state_t)&state, &count); 248 | VaccineFailOnError(thread_get_state); 249 | int64_t tmp = (int64_t)state.__rax; 250 | ax = tmp; 251 | } 252 | 253 | return ax; 254 | } 255 | 256 | /** 257 | * Loads a dynamic library at |shared_library_path| into the given |target|. 258 | * 259 | * @discussion 260 | * It calls dlopen() inside a target by creating a remote thread, setting an 261 | * expection handler on it and catching any (expected) exceptions for rewinding 262 | * the thread. 263 | * 264 | * @return see dlopen() return value. 265 | */ 266 | static 267 | int64_t task_loadlib(task_t target, const char *shared_library_path) 268 | { 269 | assert(target), assert(shared_library_path); 270 | 271 | // Allocate enough memory for dlopen()'s first argument 272 | size_t remote_path_len = strlen(shared_library_path) + 1; 273 | mach_vm_address_t remote_path = 0; 274 | int err = mach_vm_allocate(target, &remote_path, remote_path_len, 275 | VM_FLAGS_ANYWHERE); 276 | VaccineFailOnError(mach_vm_allocate); 277 | // Copy the payload path string into the target address space 278 | err = mach_vm_write(target, remote_path, (vm_offset_t)shared_library_path, 279 | (mach_msg_type_number_t)remote_path_len); 280 | VaccineFailOnError(mach_vm_write); 281 | // Allocate a remote stack (see kVaccineRemoteStackSize) 282 | mach_vm_address_t remote_stack = 0; 283 | err = mach_vm_allocate(target, &remote_stack, kVaccineRemoteStackSize, 284 | VM_FLAGS_ANYWHERE); 285 | VaccineFailOnError(mach_vm_allocate); 286 | // Configure a remote thread 287 | thread_act_t remote_thread = {0}; 288 | thread_state_flavor_t flavor = task_thread_flavor(target); 289 | if (flavor == -1) err = KERN_FAILURE; 290 | VaccineFailOnError(task_thread_flavor); 291 | 292 | if (flavor == x86_THREAD_STATE32) { 293 | remote_thread = vaccine_thread32(target, remote_stack, remote_path); 294 | } else { 295 | remote_thread = vaccine_thread64(target, remote_stack, remote_path); 296 | } 297 | if (!remote_thread) err = KERN_FAILURE; 298 | VaccineFailOnError(task_vaccine_threadXX); 299 | 300 | // Setup an exception port for the thread 301 | mach_port_t exception_port = 0; 302 | err = mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, 303 | &exception_port); 304 | VaccineFailOnError(mach_port_allocate); 305 | err = mach_port_insert_right(mach_task_self(), exception_port, 306 | exception_port, MACH_MSG_TYPE_MAKE_SEND); 307 | VaccineFailOnError(mach_port_insert_right); 308 | err = thread_set_exception_ports(remote_thread, EXC_MASK_BAD_ACCESS, 309 | exception_port, EXCEPTION_STATE_IDENTITY, 310 | flavor); 311 | VaccineFailOnError(thread_set_exception_ports); 312 | 313 | err = thread_resume(remote_thread); 314 | VaccineFailOnError(thread_resume); 315 | 316 | // Run the exception handling loop 317 | int64_t return_value = 0; 318 | while (1) { 319 | err = vaccine_catch_exception(exception_port); 320 | VaccineFailOnError(vaccine_catch_exception); 321 | 322 | int suspended = 0; 323 | err = thread_was_suspended(remote_thread, &suspended); 324 | VaccineFailOnError(thread_was_suspended); 325 | if (!suspended) continue; 326 | 327 | // OK, so our remote thread is done and we can grab a dlopen() 328 | // return value from |eax/rax| register then. 329 | return_value = thread_get_ax_register(remote_thread, flavor); 330 | // aaaaaand we've done our trip! Let's clean everything up 331 | err = thread_terminate(remote_thread); 332 | VaccineFailOnError(thread_terminate); 333 | err = mach_vm_deallocate(target, remote_path, remote_path_len); 334 | VaccineFailOnError(mach_vm_deallocate); 335 | err = mach_vm_deallocate(target, remote_stack, kVaccineRemoteStackSize); 336 | VaccineFailOnError(mach_vm_deallocate); 337 | err = mach_port_deallocate(mach_task_self(), exception_port); 338 | VaccineFailOnError(mach_port_deallocate); 339 | // bye! 340 | break; 341 | } 342 | 343 | return return_value; 344 | } 345 | 346 | /** 347 | * Injects a dynamic library at |payload_path| into the given |target|. 348 | * 349 | * @return KERN_SUCCESS 350 | * The payload was loaded successfully into the target task. 351 | * @return KERN_FAILURE 352 | * Something gone totally wrong on a task_vaccine’s side. 353 | * @return KERN_INVALID_TASK 354 | * dlopen() was unable to load the given library into the given target 355 | * (because of invalid library path, invalid target architecture, etc). 356 | */ 357 | kern_return_t task_vaccine(task_t target, const char *payload_path) 358 | { 359 | assert(target); 360 | assert(payload_path); 361 | 362 | int64_t return_value = task_loadlib(target, payload_path); 363 | // dlopen() should return a library handle pointer which is greater than zero 364 | if (return_value > 0) { 365 | return KERN_SUCCESS; 366 | } else if (return_value == 0) { 367 | // dlopen() returns zero when it can't load a library 368 | return KERN_INVALID_TASK; 369 | } 370 | 371 | return KERN_FAILURE; 372 | } 373 | 374 | /** 375 | * Catches an i386 thread exception and reconfigurates it to either call 376 | * dlopen() on it or suspend it, so task_vaccine() can finish it's job. 377 | * 378 | * @return general Mach exception handler errors: 379 | * 380 | * KERN_SUCCESS indicates that we've reset a thread's state and it's ready to 381 | * be resumed. 382 | * 383 | * MIG_NO_REPLY indicates that we've terminated or suspended the thread, so the 384 | * kernel won't handle it anymore. This one also breaks our exception handling 385 | * loop, so we able to finish the injection process. 386 | * 387 | * KERN_FAILURE indicates that we weren't expecting this kind of exception and 388 | * the kernel should take care of it. 389 | */ 390 | static 391 | kern_return_t catch_i386_exception(task_t task, mach_port_t thread, 392 | x86_thread_state32_t *in_state, 393 | x86_thread_state32_t *out_state) 394 | { 395 | assert(task), assert(thread), assert(in_state), assert(out_state); 396 | 397 | if (in_state->__eip == kVaccineFinishReturnValue) { 398 | // OK, Glass, finish here 399 | thread_suspend(thread); 400 | return MIG_NO_REPLY; 401 | } else if (in_state->__eip != kVaccineJumpToDlopenReturnValue) { 402 | // Oops, we broke something up, notify a user 403 | goto error; 404 | } 405 | // Well, setup a thread to execute dlopen() with a given library path 406 | memcpy(out_state, in_state, sizeof(*in_state)); 407 | uint32_t dlopen_addr = lorgnette_lookup(task, "dlopen"); 408 | out_state->__eip = dlopen_addr; 409 | out_state->__esp = ({ 410 | mach_vm_address_t stack = in_state->__esp - (sizeof(uint32_t)); 411 | stack -= 4; // simulate the call instruction 412 | int mode = RTLD_NOW | RTLD_LOCAL; 413 | uint32_t local_stack[] = { 414 | kVaccineFinishReturnValue, 415 | in_state->__ebx, // we hold a library path here 416 | mode 417 | }; 418 | int err = mach_vm_write(task, stack, (mach_vm_offset_t)local_stack, 419 | sizeof(local_stack)); 420 | if (err != KERN_SUCCESS) { 421 | syslog(LOG_NOTICE, "[%d] mach_vm_write() failed: %d (%s)\n", 422 | __LINE__-4, err, mach_error_string(err)); 423 | goto error; 424 | } 425 | stack; 426 | }); 427 | 428 | return KERN_SUCCESS; 429 | 430 | error: 431 | memset(out_state, 0, sizeof(*out_state)); 432 | out_state->__eax = (0); 433 | // Since we return MIG_NO_REPLY the kernel won't update the thread's 434 | // state, so we have to do it manually 435 | thread_set_state(thread, x86_THREAD_STATE32, (thread_state_t)out_state, 436 | x86_THREAD_STATE32_COUNT); 437 | thread_suspend(thread); 438 | return MIG_NO_REPLY; 439 | } 440 | 441 | /** 442 | * @see catch_i386_exception() 443 | */ 444 | static 445 | kern_return_t catch_x86_64_exception(task_t task, mach_port_t thread, 446 | x86_thread_state64_t *in_state, 447 | x86_thread_state64_t *out_state) 448 | { 449 | assert(task), assert(thread), assert(in_state), assert(out_state); 450 | 451 | if (in_state->__rip == kVaccineFinishReturnValue) { 452 | // OK, Glass, finish here 453 | thread_suspend(thread); 454 | return MIG_NO_REPLY; 455 | } else if (in_state->__rip != kVaccineJumpToDlopenReturnValue) { 456 | // Oops, we broke something up, notify a user 457 | goto error; 458 | } 459 | // Well, setup a thread to execute dlopen() with a given library path 460 | memcpy(out_state, in_state, sizeof(*in_state)); 461 | uint64_t dlopen_addr = lorgnette_lookup(task, "dlopen"); 462 | out_state->__rip = dlopen_addr; 463 | out_state->__rsi = RTLD_NOW | RTLD_LOCAL; 464 | out_state->__rdi = in_state->__rbx; // we hold a library path here 465 | out_state->__rsp = ({ 466 | mach_vm_address_t stack = in_state->__rsp; 467 | stack -= 8; // simulate the call instruction 468 | vm_offset_t new_ret_value_ptr = (vm_offset_t)&(uint64_t){ 469 | kVaccineFinishReturnValue 470 | }; 471 | int err = mach_vm_write(task, stack, new_ret_value_ptr, 472 | sizeof(kVaccineFinishReturnValue)); 473 | if (err != KERN_SUCCESS) { 474 | syslog(LOG_NOTICE, "[%d] mach_vm_write() failed: %d (%s)\n", 475 | __LINE__-5, err, mach_error_string(err)); 476 | goto error; 477 | } 478 | stack; 479 | }); 480 | 481 | return KERN_SUCCESS; 482 | 483 | error: 484 | memset(out_state, 0, sizeof(*out_state)); 485 | out_state->__rax = (0); 486 | // Since we return MIG_NO_REPLY the kernel won't update the thread's 487 | // state, so we have to do it manually 488 | thread_set_state(thread, x86_THREAD_STATE64, (thread_state_t)out_state, 489 | x86_THREAD_STATE64_COUNT); 490 | thread_suspend(thread); 491 | return MIG_NO_REPLY; 492 | } 493 | 494 | /** 495 | * see catch_i386_exception() and catch_x86_64_exception() 496 | */ 497 | __attribute__((visibility("default"))) 498 | kern_return_t 499 | catch_exception_raise_state_identity(mach_port_t exception_port, 500 | mach_port_t thread, 501 | mach_port_t task, 502 | exception_type_t exception, 503 | exception_data_t code, 504 | mach_msg_type_number_t code_count, 505 | int *flavor, thread_state_t in_state, 506 | mach_msg_type_number_t in_state_count, 507 | thread_state_t out_state, 508 | mach_msg_type_number_t *out_state_count) 509 | { 510 | #pragma unused (exception_port, exception, code, code_count) 511 | #pragma unused (in_state_count, out_state, out_state_count) 512 | 513 | if (*flavor == x86_THREAD_STATE64) { 514 | x86_thread_state64_t *in_state64 = (x86_thread_state64_t *)in_state; 515 | x86_thread_state64_t *out_state64 = (x86_thread_state64_t *)out_state; 516 | 517 | *out_state_count = x86_THREAD_STATE64_COUNT; 518 | return catch_x86_64_exception(task, thread, in_state64, out_state64); 519 | 520 | } else if (*flavor == x86_THREAD_STATE32) { 521 | x86_thread_state32_t *in_state32 = (x86_thread_state32_t *)in_state; 522 | x86_thread_state32_t *out_state32 = (x86_thread_state32_t *)out_state; 523 | 524 | *out_state_count = x86_THREAD_STATE32_COUNT; 525 | return catch_i386_exception(task, thread, in_state32, out_state32); 526 | } 527 | 528 | // It's not i386 nor x86_64 so we have nothing to do with it 529 | return KERN_FAILURE; 530 | } 531 | -------------------------------------------------------------------------------- /task_vaccine.h: -------------------------------------------------------------------------------- 1 | // task_vaccine.h 2 | // Copyright (c) 2014 Dmitry Rodionov 3 | // https://github.com/rodionovd/task_vaccines 4 | // 5 | // This software may be modified and distributed under the terms 6 | // of the MIT license. See the LICENSE file for details. 7 | 8 | #pragma once 9 | #include 10 | 11 | #ifdef __cplusplus 12 | extern "C" { 13 | #endif 14 | /** 15 | * Injects the |target| task with a shared library at |payload_path|. 16 | * 17 | * @param target 18 | * An identifier of the target process. 19 | * @param payload_path 20 | * A full or relative path to the library you want to load into the given 21 | * target. The library should be compatible with the target task. In order to be 22 | * meaningful, this library should also contain a constructor function that will 23 | * be executed on load. 24 | * 25 | * @return KERN_SUCCESS 26 | * The payload was loaded successfully into the target task. 27 | * @return KERN_INVALID_TASK 28 | * dlopen() failed to load the given library (e.g. target architecture doesn't 29 | * match the payload). 30 | * @return KERN_FAILURE 31 | * Something gone totally wrong on a task_vaccine’s side. Please open an issue 32 | * on GitHub. 33 | */ 34 | kern_return_t task_vaccine(task_t target, const char *payload_path); 35 | 36 | #ifdef __cplusplus 37 | } 38 | #endif 39 | -------------------------------------------------------------------------------- /tests/Rakefile: -------------------------------------------------------------------------------- 1 | task :default => ["task_vaccine_tests:bootstrap", 2 | "task_vaccine_tests:run_tests", "task_vaccine_tests:cleanup", 3 | "task_vaccine_tests:exit"] 4 | 5 | namespace :task_vaccine_tests do 6 | task :default => [:bootstrap, :run_tests, :cleanup, :exit] 7 | # # # # # # # # # # # # # # # 8 | # SPECS 9 | # # # # # # # # # # # # # # # 10 | if (File.basename(Dir.pwd) == "tests") then 11 | TESTS_SOURCES = "tests.c ../task_vaccine.c \ 12 | ../submodules/liblorgnette/lorgnette.c" 13 | PAYLOAD_SOURCES = "demo_payload.c" 14 | TARGET_SOURCES = "demo_target.c" 15 | else 16 | # In case we execute tasks from within the project's main Rakefile 17 | TESTS_SOURCES = "./tests/tests.c ./task_vaccine.c \ 18 | ./submodules/liblorgnette/lorgnette.c" 19 | PAYLOAD_SOURCES = "./tests/demo_payload.c" 20 | TARGET_SOURCES = "./tests/demo_target.c" 21 | end 22 | 23 | TESTS_SHARED_CCFLAGS = "" 24 | TESTS_NEED_SUDO = true 25 | test_specs = [ 26 | {:arch => "i386", :ccflags => "-O0" }, 27 | {:arch => "x86_64", :ccflags => "-O0" }, 28 | ] 29 | payload_specs = [ 30 | # i386 31 | { :arch => ["i386"], :ccflags => "" }, 32 | # x86_64 33 | { :arch => ["x86_64"], :ccflags => "" }, 34 | # Fat (both i386 and x86_64) 35 | { :arch => ["i386", "x86_64"], :ccflags => "" }, 36 | ] 37 | target_specs = [ 38 | { :arch => "i386", :ccflags => "" }, 39 | { :arch => "x86_64", :ccflags => "" }, 40 | ] 41 | 42 | # # # # # # # # # # # # # # # 43 | # TASKS 44 | # # # # # # # # # # # # # # # 45 | 46 | desc "Prepare the environment" 47 | task :bootstrap do 48 | system("mkdir build_tests") if (!File.exists?(Dir.getwd + "/build_tests")) 49 | end 50 | 51 | desc "Build tests cases" 52 | task :build_tests do 53 | test_specs.each {|entry| 54 | system("clang -arch #{entry[:arch]} \ 55 | #{TESTS_SHARED_CCFLAGS} \ 56 | #{entry[:ccflags]} \ 57 | -o ./build_tests/#{filename_for_test_spec(entry)} \ 58 | #{TESTS_SOURCES}") 59 | if $?.exitstatus == 0 then 60 | entry[:result] = true; 61 | else 62 | entry[:result] = false; 63 | puts "Fail to compile test suit: '#{outfile}'. Aborting...".red 64 | end 65 | } 66 | end 67 | 68 | desc "Build demo payloads" 69 | task :build_payloads do 70 | payload_specs.each {|entry| 71 | arch_string = "-arch " + entry[:arch].join(" -arch ") 72 | system("clang #{arch_string} \ 73 | -dynamiclib -single_module \ 74 | -o ./build_tests/#{filename_for_payload_spec(entry)} \ 75 | #{PAYLOAD_SOURCES}") 76 | if $?.exitstatus != 0 then 77 | puts "Fail to compile payload: '#{outfile}'. Aborting...".red 78 | exit (-1) 79 | end 80 | } 81 | end 82 | 83 | desc "Build demo targets" 84 | task :build_targets do 85 | target_specs.each {|entry| 86 | system("clang -arch #{entry[:arch]} \ 87 | #{entry[:ccflags]} \ 88 | -o ./build_tests/#{filename_for_target_spec(entry)} \ 89 | #{TARGET_SOURCES}") 90 | if $?.exitstatus == 0 then 91 | entry[:result] = true; 92 | else 93 | entry[:result] = false; 94 | puts "Fail to compile target: '#{outfile}'. Aborting...".red 95 | end 96 | } 97 | end 98 | 99 | desc "Run test cases" 100 | task :run_tests => [:build_tests, :build_payloads, :build_targets] do 101 | test_specs.each {|entry| 102 | if entry[:result] != false then 103 | sudo_prefix = "sudo " if TESTS_NEED_SUDO 104 | system("#{sudo_prefix}./build_tests/#{filename_for_test_spec(entry)}") 105 | entry[:result] = false if $?.exitstatus != 0 106 | 107 | status = (entry[:result] == true) ? "OK".green : "FAIL".red 108 | printf " %s\n", 109 | "#{entry[:arch]}/#{entry[:ccflags].sub(' ', ',')}".brown, 110 | status 111 | end 112 | } 113 | end 114 | 115 | desc "Run the tests again and again until somebody hits ^C" 116 | task :inferno do 117 | Rake::Task["task_vaccine_tests:bootstrap"].invoke 118 | Rake::Task["task_vaccine_tests:build_tests"].invoke 119 | Rake::Task["task_vaccine_tests:build_targets"].invoke 120 | Rake::Task["task_vaccine_tests:build_payloads"].invoke 121 | i = 1 122 | while true do 123 | Rake::Task["task_vaccine_tests:run_tests"].execute 124 | puts "test n. #{i}" 125 | i = i+1 126 | end 127 | # won't be reached anyway 128 | Rake::Task["task_vaccine_tests:cleanup"].invoke 129 | end 130 | 131 | desc "Clean up /build/ directory" 132 | task :cleanup do 133 | system("rm -Rf ./build_tests") 134 | end 135 | 136 | desc "Finish testing" 137 | task :exit => [:run_tests] do 138 | if test_specs.index {|e| e[:result] == false} != nil then 139 | exit (-1) 140 | else 141 | exit (0) 142 | end 143 | end 144 | 145 | 146 | # # # # # # # # # # # # # # # 147 | # MISC 148 | # # # # # # # # # # # # # # # 149 | 150 | def filename_for_test_spec(s) 151 | "tests.#{s[:arch]}#{s[:ccflags].delete(' ')}" 152 | end 153 | 154 | def filename_for_target_spec(s) 155 | "target.#{s[:arch]}" 156 | end 157 | 158 | def filename_for_payload_spec(s) 159 | "payload.#{s[:arch].join('.')}" 160 | end 161 | 162 | # Colors, Rainbows, Unicorns! 163 | class String 164 | def red; "\033[1;31m#{self}\033[0m" end 165 | def green; "\033[1;32m#{self}\033[0m" end 166 | def brown; "\033[0;33m#{self}\033[0m" end 167 | end 168 | end 169 | -------------------------------------------------------------------------------- /tests/demo_payload.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | __attribute__((constructor)) 5 | void sayhello(void) 6 | { 7 | fprintf(stdout, "Hello from [%d]\n", getpid()); 8 | } 9 | -------------------------------------------------------------------------------- /tests/demo_target.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | int main(int argc, char const *argv[]) 5 | { 6 | fprintf(stdout, "Target: %d\n", getpid()); 7 | /* Wait for injection */ 8 | dispatch_main(); 9 | 10 | return 0; 11 | } 12 | -------------------------------------------------------------------------------- /tests/tests.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include "../task_vaccine.h" 6 | #include "../submodules/Cegta/Cegta.h" 7 | 8 | #define kTargetInitializationTimeMSec 300000 9 | 10 | SpecBegin(task_vaccine) 11 | 12 | task_t (^_launch_target)(const char *) = ^(const char *target) { 13 | task_t task = (-1); 14 | pid_t pid = fork(); 15 | if (pid == 0) { 16 | execl(target, NULL); 17 | fprintf(stderr, "Unable to execv() the target <%s> due to error: %s\n", 18 | target, strerror(errno)); 19 | exit(EXIT_FAILURE); 20 | } else { 21 | usleep(kTargetInitializationTimeMSec); // let it initialize a bit 22 | int err = task_for_pid(mach_task_self(), pid, &task); 23 | assert(err == KERN_SUCCESS); 24 | } 25 | return (task); 26 | }; 27 | 28 | void (^_kill_target)(task_t target) = ^(task_t target) { 29 | pid_t pid; 30 | int err = pid_for_task(target, &pid); 31 | assert(err == KERN_SUCCESS); 32 | kill(pid, SIGTERM); 33 | }; 34 | 35 | describe("i386 target", ^{ 36 | __block task_t target = (-1); 37 | 38 | beforeEach(^(const char *it) { 39 | target = _launch_target("./build_tests/target.i386"); 40 | }); 41 | 42 | afterEach(^(const char *it) { 43 | _kill_target(target); 44 | }); 45 | 46 | it("should be injected with i386 payload", ^{ 47 | requireInt(target, notToBe(-1)); 48 | 49 | int err = task_vaccine(target, "./build_tests/payload.i386"); 50 | expectInt(err, toBe(KERN_SUCCESS)); 51 | }); 52 | it("should be injected with FAT payload", ^{ 53 | requireInt(target, notToBe(-1)); 54 | 55 | int err = task_vaccine(target, "./build_tests/payload.i386.x86_64"); 56 | expectInt(err, toBe(KERN_SUCCESS)); 57 | }); 58 | it("should NOT be injected with x86_64 payload", ^{ 59 | requireInt(target, notToBe(-1)); 60 | 61 | int err = task_vaccine(target, "./build_tests/payload.x86_64"); 62 | expectInt(err, toBe(KERN_INVALID_TASK)); 63 | }); 64 | }); 65 | 66 | describe("x86_64 target",^{ 67 | 68 | __block task_t target = (-1); 69 | 70 | beforeEach(^(const char *it) { 71 | target = _launch_target("./build_tests/target.x86_64"); 72 | }); 73 | 74 | afterEach(^(const char *it) { 75 | _kill_target(target); 76 | }); 77 | 78 | it("should be injected with x86_64 payload", ^{ 79 | requireInt(target, notToBe(-1)); 80 | 81 | int err = task_vaccine(target, "./build_tests/payload.x86_64"); 82 | expectInt(err, toBe(KERN_SUCCESS)); 83 | }); 84 | it("should be injected with FAT payload", ^{ 85 | requireInt(target, notToBe(-1)); 86 | 87 | int err = task_vaccine(target, "./build_tests/payload.i386.x86_64"); 88 | expectInt(err, toBe(KERN_SUCCESS)); 89 | }); 90 | 91 | it("should NOT be injected with i386 payload", ^{ 92 | requireInt(target, notToBe(-1)); 93 | 94 | int err = task_vaccine(target, "./build_tests/payload.i386"); 95 | expectInt(err, toBe(KERN_INVALID_TASK)); 96 | }); 97 | }); 98 | 99 | SpecEnd() 100 | 101 | 102 | CegtaRun(); 103 | --------------------------------------------------------------------------------