├── .gitignore ├── LICENSE ├── README.md ├── dtxmsg.cpp ├── dtxmsg.h ├── dtxmsg_client.cpp ├── dtxmsg_client.h ├── dtxmsg_common.cpp ├── dtxmsg_common.h ├── makefile └── slides.pdf /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | *.swo 3 | *tags 4 | obj/* 5 | *.~lock* 6 | *.id0 7 | *.id1 8 | *.nam 9 | *.til 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Hex-Rays 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # dtxmsg 2 | 3 | This is an IDA plugin that helped me reverse-engineer the DTXConnectionServices framework. 4 | 5 | This plugin was a core topic of my presentation [Discovering the iOS Instruments Server][4] 6 | at [Recon Montreal 2018][5]. 7 | 8 | ## Overview 9 | 10 | DTXConnectionServices is a library developed by Apple that facilitates interoperability 11 | between iOS and OSX. It is notably used to transmit debugging statistics between the 12 | iOS Instruments Server and Xcode. 13 | 14 | The goal of this plugin is to help uncover how this communication mechanism works. 15 | 16 | dtxmsg detects critical pieces of logic in the DTXConnectionServices binary, sets breakpoints 17 | at these locations, then hooks into IDA's debugger events and dumps the packets of information 18 | transmitted between iOS and OSX. 19 | 20 | Apple calls these packets "DTXMessages", hence the name of the plugin. 21 | 22 | The plugin can also decode these messages and print the contents to a file in plain text. 23 | 24 | ## Prerequisites 25 | 26 | In order to build and run dtxmsg, you must have access to the following: 27 | 28 | * IDA 7.1 or later, with decompiler support 29 | * IDA SDK 7.1 or later 30 | * hexrays\_sdk 7.1 or later 31 | * a jailbroken iOS device (this is required for debugging the Instruments server. 32 | however, simply *communicating* with the server does not require a jailbroken device. see [dtxmsg_client](#dtxmsg_client)) 33 | * a patched iOS [debugserver][1] 34 | * OSX with Xcode installed 35 | 36 | This plugin was tested with iOS 9.3.1 and OSX 10.13. 37 | 38 | Theoretically, the plugin can work with any iOS between 9.3-11.4, and any OSX between 10.10-10.13, 39 | but these have not been explicitly tested. 40 | 41 | ## Build 42 | 43 | To build dtxmsg, run the following commands: 44 | 45 | ``` 46 | $ export IDA_INSTALL_DIR=/path/to/your/IDA/installation 47 | $ export IDASDK=/path/to/your/idasdk 48 | $ cd $IDASDK/plugins 49 | $ git clone https://github.com/troybowman/dtxmsg 50 | $ cd dtxmsg 51 | $ NDEBUG=1 $IDASDK/bin/idamake.pl 52 | $ __EA64__=1 NDEBUG=1 $IDASDK/bin/idamake.pl 53 | ``` 54 | 55 | ## Run 56 | 57 | To demonstrate the dtxmsg plugin in action, we will use it to log all the messages 58 | received by the iOS Instruments Server when Xcode queries the process list 59 | ("Debug>Attach to Process" in the Xcode IDE). 60 | 61 | 1. It may be a good idea brush up on [how IDA's iOS Debugger Works][2] 62 | 63 | 2. Launch Xcode, and open an iOS project (make sure your device is selected as the build target) 64 | 65 | 3. download [ios_deploy][3] and run the following commands: 66 | ``` 67 | $ ios_deploy -d usbproxy -r 22 -l 2222 & 68 | $ ios_deploy -d usbproxy -r 1234 -l 4321 & 69 | $ ssh -p 2222 root@localhost 70 | Connected to port 22 on device 71 | iPhone-6-jailbroken:~ root# ps aux | grep DTServiceHub 72 | root 11451 0.0 0.5 712144 10960 ?? Ss Tue04PM 0:02.91 /Developer/Library/PrivateFrameworks/DVTInstrumentsFoundation.framework/DTServiceHub 73 | iPhone-6-jailbroken:~ root# ./debugserver *:1234 74 | debugserver-@(#)PROGRAM:debugserver PROJECT:debugserver-340.3.124 for arm64. 75 | Listening to port 1234 for a connection from *... 76 | ``` 77 | Note the PID of the application DTServiceHub (11451). This is the Instruments Server process. 78 | If this process is not running, go to your Xcode window and select menu Debug>Attach to process. 79 | This should launch the Instruments server. 80 | 81 | 4. Be sure to set the following options in $IDA\_INSTALL\_DIR/ida.app/Contents/MacOS/cfg/dbg\_ios.cfg: 82 | ``` 83 | AUTOLAUNCH = NO 84 | SYMBOL_PATH = "~/Library/Developer/Xcode/iOS DeviceSupport//Symbols" 85 | DEVICE_ID = "" 86 | ``` 87 | 88 | 5. Run the plugin in IDA: 89 | ``` 90 | $ hdiutil mount /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/DeviceSupport//DeveloperDiskImage.dmg 91 | $ mkdir /tmp/dtxmsg 92 | $ $IDA_INSTALL_DIR/ida.app/Contents/MacOS/ida64 -Odtxmsg:11451:/tmp/dtxmsg:v -o/tmp/dtxmsg/DTXConnectionServices.i64 -L/tmp/dtxmsg/ida.log /Volumes/DeveloperDiskImage/Library/PrivateFrameworks/DTXConnectionServices.framework/DTXConnectionServices 93 | ``` 94 | Note the plugin options: -Odtxmsg:11451:/tmp/dtxmsg:v 95 | * 11451 = PID of the Instruments Server process 96 | * /tmp/dtxmsg = directory where messages will be logged (must be an absolute path) 97 | * v = enable verbose mode. captured messages will be deserialized and printed to a file in plain text 98 | 99 | If the plugin loads successfully, it will automatically attach to the given PID and allow the process to run idle, 100 | waiting for incoming messages. 101 | 102 | 6. Go back to Xcode, and select menu Debug>Attach to Process. If dtxmsg was able to intercept communications, 103 | it will print some messages to the console: 104 | ``` 105 | DTXMSG: message: /tmp/dtxmsg/dtxmsg_1_0.bin 106 | DTXMSG: message: /tmp/dtxmsg/dtxmsg_2_0.bin 107 | DTXMSG: message: /tmp/dtxmsg/dtxmsg_3_0.bin 108 | ... 109 | ``` 110 | There will also be .txt files that contain the decoded data. 111 | 112 | ## dtxmsg_client 113 | 114 | This project also includes a [standalone application][6] that can communicate with the iOS Instruments 115 | Server independently. It serves as an example of how to "speak the language" 116 | of the DTXConnectionServices framework. 117 | 118 | This app can communicate with any device, provided the Instruments Server has been installed. 119 | The app does not require a jailbreak, and so far has worked with any iOS version from 9.3-12.0. 120 | 121 | To run it, see: 122 | 123 | ``` 124 | $ $IDASDK/bin/dtxmsg_client -h 125 | ``` 126 | 127 | Note that you can install the Instruments Server with [ios_deploy][3]: 128 | ``` 129 | $ ios_deploy mount -h 130 | ``` 131 | 132 | UPDATE: the dtxmsg\_client app has been moved to its own repository: [ios\_instruments\_client][7]. 133 | Due to the popularity of this client code, it made sense to create a standalone repo that does not depend on the IDA SDK to build. 134 | For any inquiries about the dtxmsg\_client app, please use the ios\_instruments\_client repo instead. 135 | 136 | [1]: http://iphonedevwiki.net/index.php/Debugserver 137 | [2]: https://www.hex-rays.com/products/ida/support/tutorials/ios_debugger_tutorial.pdf 138 | [3]: https://www.hex-rays.com/products/ida/support/ida/ios_deploy.zip 139 | [4]: https://github.com/troybowman/dtxmsg/blob/master/slides.pdf 140 | [5]: https://recon.cx/2018/montreal 141 | [6]: https://github.com/troybowman/dtxmsg/blob/master/dtxmsg_client.cpp 142 | [7]: https://github.com/troybowman/ios_instruments_client 143 | -------------------------------------------------------------------------------- /dtxmsg.cpp: -------------------------------------------------------------------------------- 1 | #include "dtxmsg.h" 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | //----------------------------------------------------------------------------- 11 | bool dtxmsg_plugmod_t::deserialize_component( 12 | const char *label, 13 | uint32 identifier, 14 | FILE *payload_fp, 15 | uint64 off, 16 | uint64 length, 17 | bool is_array) const 18 | { 19 | qstring base; 20 | char binpath[QMAXPATH]; 21 | 22 | // save the raw data to a file for reference 23 | base.sprnt("%s_%d.bin", label, identifier); 24 | qmakepath(binpath, sizeof(binpath), logdir, base.c_str(), NULL); 25 | 26 | FILE *binfp = qfopen(binpath, "wb"); 27 | if ( binfp == NULL ) 28 | { 29 | dtxmsg_deb("Error: failed to open %s: %s\n", binpath, winerr(errno)); 30 | return false; 31 | } 32 | file_janitor_t bj(binfp); 33 | 34 | bytevec_t bytes; 35 | bytes.resize(length); 36 | 37 | // read the component from the complete payload, 38 | // and write it out to a separate file. 39 | if ( qfseek(payload_fp, off, SEEK_SET) != 0 40 | || qfread(payload_fp, bytes.begin(), length) != length 41 | || qfwrite(binfp, bytes.begin(), length) != length ) 42 | { 43 | dtxmsg_deb("Error: failed to create %s: %s\n", binpath, winerr(errno)); 44 | return false; 45 | } 46 | 47 | dtxmsg_deb("%s: %s\n", label, binpath); 48 | 49 | char txtpath[QMAXPATH]; 50 | set_file_ext(txtpath, sizeof(txtpath), binpath, "txt"); 51 | 52 | // try to deserialize the component and save it in plain text 53 | FILE *txtfp = qfopen(txtpath, "w"); 54 | if ( txtfp == NULL ) 55 | { 56 | dtxmsg_deb("Error: failed to create %s: %s\n", txtpath, winerr(errno)); 57 | return false; 58 | } 59 | file_janitor_t tj(txtfp); 60 | 61 | if ( length != 0 ) 62 | { 63 | CFTypeRef obj = NULL; 64 | qstring errbuf = "unarchive() failed"; 65 | 66 | if ( is_array ) 67 | obj = deserialize(bytes.begin(), length, &errbuf); 68 | else 69 | obj = unarchive(bytes.begin(), length); 70 | 71 | if ( obj == NULL ) 72 | { 73 | dtxmsg_deb("Error: failed to deserialize %s: %s\n", binpath, errbuf.c_str()); 74 | return false; 75 | } 76 | 77 | qfprintf(txtfp, "%s\n", get_description(obj).c_str()); 78 | CFRelease(obj); 79 | } 80 | 81 | dtxmsg_deb("%s: %s\n", label, txtpath); 82 | return true; 83 | } 84 | 85 | //----------------------------------------------------------------------------- 86 | bool dtxmsg_plugmod_t::deserialize_payload(const char *path, uint32 id) const 87 | { 88 | FILE *payload_fp = qfopen(path, "rb"); 89 | if ( payload_fp == NULL ) 90 | { 91 | dtxmsg_deb("Error: failed to open payload file %s for reading: %s\n", path, winerr(errno)); 92 | return false; 93 | } 94 | file_janitor_t pj(payload_fp); 95 | 96 | DTXMessagePayloadHeader pheader; 97 | if ( qfread(payload_fp, &pheader, sizeof(pheader)) != sizeof(pheader) ) 98 | { 99 | dtxmsg_deb("Error: failed to read payload header from %s: %s\n", path, winerr(errno)); 100 | return false; 101 | } 102 | 103 | uint8 message_type = pheader.flags & 0xFF; 104 | uint8 compression_type = pheader.flags & 0xFF000 >> 12; 105 | uint32 algorithm = 0; 106 | 107 | if ( message_type == 6 ) 108 | { 109 | dtxmsg_deb("Error: message payload is a serialized dictionary. we only know how to deserialize arrays.\n"); 110 | return false; 111 | } 112 | 113 | // it seems Xcode does not normally use compression when sending messages to the instruments server. 114 | // for now we will assume that it doesn't, and throw an error if compression is detected. 115 | if ( message_type == 7 && compression_type > 3 ) 116 | { 117 | dtxmsg_deb("Error: message is compressed (compression_type=%x). We must uncompress it before we read it!\n", compression_type); 118 | return false; 119 | } 120 | 121 | asize_t auxlen = pheader.auxiliaryLength; 122 | asize_t objlen = pheader.totalLength - auxlen; 123 | 124 | // the payload is broken up into two components: 125 | // 1. the payload object (a single archived NSObject) 126 | // 2. auxiliary data (a serialized array of archived NSObjects) 127 | return deserialize_component("auxiliary", id, payload_fp, sizeof(pheader), auxlen, true) 128 | && deserialize_component("object", id, payload_fp, sizeof(pheader) + auxlen, objlen, false); 129 | } 130 | 131 | //----------------------------------------------------------------------------- 132 | static bool format_header(qstring *out, ea_t ea, const tinfo_t &tif) 133 | { 134 | format_data_info_t fdi; 135 | fdi.ptvf = PTV_SPACE|PTV_QUEST|PTV_CSTR|PTV_DEBUG|PTV_DEREF; 136 | fdi.radix = 16; 137 | fdi.margin = 0; 138 | fdi.max_length = MAXSTR; 139 | 140 | argloc_t loc; 141 | loc.set_ea(ea); 142 | 143 | idc_value_t idcv; 144 | idcv.vtype = VT_PVOID; 145 | idcv.pvoid = &loc; 146 | 147 | qstrvec_t outvec; 148 | if ( !format_cdata(&outvec, idcv, &tif, NULL, &fdi) ) 149 | return false; 150 | 151 | *out = outvec[0]; 152 | return true; 153 | } 154 | 155 | //----------------------------------------------------------------------------- 156 | bool dtxmsg_plugmod_t::parse_message(ea_t buf, const DTXMessageHeader &mheader) const 157 | { 158 | // it is possible that this message is just one of many "fragments". 159 | // this happens when an object is too big to be transmitted in a single message 160 | // and is therefore split up across multiple messages. so, we always append the 161 | // current fragment to an incremental payload file and deserialize the data only 162 | // after all fragments have been read. 163 | ea_t fptr = buf + sizeof(mheader); 164 | ea_t flen = mheader.length; 165 | 166 | // the first fragment contains the payload header. 167 | // note that if a payload does not require multiple fragments, the fragmentId is 0. 168 | if ( mheader.fragmentId <= 1 ) 169 | { 170 | tinfo_t tif; 171 | if ( !tif.get_named_type(NULL, "DTXMessagePayloadHeader") ) 172 | { 173 | dtxmsg_deb("Error: failed to retrieve tinfo for DTXMessagePayloadHeader\n"); 174 | return false; 175 | } 176 | 177 | qstring hstr; 178 | if ( !format_header(&hstr, fptr, tif) ) 179 | { 180 | dtxmsg_deb("Error: failed to format DTXMessagePayloadHeader at %a\n", fptr); 181 | return false; 182 | } 183 | 184 | qfprintf(headers_fp, "\t- DTXMessagePayloadHeader: %s\n", hstr.c_str()); 185 | qflush(headers_fp); 186 | } 187 | 188 | bytevec_t fbytes; 189 | fbytes.resize(flen); 190 | if ( flen != 0 && read_dbg_memory(fptr, fbytes.begin(), flen) != flen ) 191 | { 192 | dtxmsg_deb("Error: failed to read message fragment at %a, size = %a\n", fptr, flen); 193 | return false; 194 | } 195 | 196 | // append this fragment to the incremental payload file. 197 | // also note that different fragments for the same payload will 198 | // have the same message identifier. 199 | qstring fname; 200 | fname.sprnt("payload_%d.bin", mheader.identifier); 201 | 202 | char path[MAXSTR]; 203 | qmakepath(path, sizeof(path), logdir, fname.c_str(), NULL); 204 | 205 | FILE *payload_fp = qfopen(path, "a"); 206 | if ( payload_fp == NULL ) 207 | { 208 | dtxmsg_deb("Error: failed to open %s: %s\n", path, winerr(errno)); 209 | return false; 210 | } 211 | 212 | qfwrite(payload_fp, fbytes.begin(), flen); 213 | qfclose(payload_fp); 214 | 215 | dtxmsg_deb("payload: %s\n", path); 216 | 217 | // after writing the last fragment, deserialize the complete payload 218 | if ( mheader.fragmentId == mheader.fragmentCount - 1 ) 219 | { 220 | if ( !deserialize_payload(path, mheader.identifier) ) 221 | return false; 222 | } 223 | 224 | return true; 225 | } 226 | 227 | //----------------------------------------------------------------------------- 228 | bool dtxmsg_plugmod_t::handle_dtxmsg_bpt(void) const 229 | { 230 | // return value is a pointer to the message buffer 231 | const char *reg; 232 | if ( ph.id == PLFM_ARM ) 233 | reg = inf_is_64bit() ? "X0" : "R0"; 234 | else 235 | reg = inf_is_64bit() ? "RAX" : "EAX"; 236 | 237 | regval_t val; 238 | if ( !get_reg_val(reg, &val) ) 239 | { 240 | dtxmsg_deb("Error: failed to get value of register %s\n", reg); 241 | return false; 242 | } 243 | 244 | ea_t buf = val.ival; 245 | 246 | // if buffer is NULL, just ignore it 247 | if ( buf == 0 ) 248 | return true; 249 | 250 | tinfo_t tif; 251 | if ( !tif.get_named_type(NULL, "DTXMessageHeader") ) 252 | { 253 | dtxmsg_deb("Error: failed to retrieve tinfo for DTXMessageHeader\n"); 254 | return false; 255 | } 256 | 257 | qstring hstr; 258 | if ( !format_header(&hstr, buf, tif) ) 259 | { 260 | dtxmsg_deb("Error: failed to format DTXMessageHeader at %a\n", buf); 261 | return false; 262 | } 263 | 264 | DTXMessageHeader mheader; 265 | if ( read_dbg_memory(buf, &mheader, sizeof(mheader)) != sizeof(mheader) ) 266 | { 267 | dtxmsg_deb("Error: failed to read DTXMessageHeader at %a\n", buf); 268 | return false; 269 | } 270 | 271 | qfprintf(headers_fp, "%d.%d: DTXMessageHeader: %s\n", mheader.identifier, mheader.fragmentId, hstr.c_str()); 272 | qflush(headers_fp); 273 | 274 | asize_t size = sizeof(mheader) + mheader.length; 275 | 276 | bytevec_t dtxmsg; 277 | dtxmsg.resize(size); 278 | if ( read_dbg_memory(buf, dtxmsg.begin(), size) != size ) 279 | { 280 | dtxmsg_deb("Error: failed to read %a bytes of DTXMessage data at %a\n", size, buf); 281 | return false; 282 | } 283 | 284 | char path[QMAXPATH]; 285 | 286 | qstring fname; 287 | fname.sprnt("dtxmsg_%d_%d.bin", mheader.identifier, mheader.fragmentId); 288 | qmakepath(path, sizeof(path), logdir, fname.c_str(), NULL); 289 | 290 | FILE *message_fp = qfopen(path, "wb"); 291 | if ( message_fp == NULL ) 292 | { 293 | dtxmsg_deb("Error: failed to open %s: %s\n", path, winerr(errno)); 294 | return false; 295 | } 296 | 297 | qfwrite(message_fp, dtxmsg.begin(), size); 298 | qfclose(message_fp); 299 | 300 | dtxmsg_deb("message: %s\n", path); 301 | 302 | // don't try to parse the message payload, unless instructed 303 | bool ok = !verbose || parse_message(buf, mheader); 304 | 305 | qfprintf(headers_fp, "\n"); 306 | qflush(headers_fp); 307 | 308 | return ok; 309 | } 310 | 311 | //----------------------------------------------------------------------------- 312 | ssize_t idaapi dbg_listener_t::on_event(ssize_t notification_code, va_list va) 313 | { 314 | switch ( notification_code ) 315 | { 316 | case dbg_bpt: 317 | { 318 | thid_t tid = va_arg(va, thid_t); 319 | ea_t bpt = va_arg(va, ea_t); 320 | 321 | if ( dpm.dtxmsg_node.altval_ea(bpt, DTXMSG_ALT_BPTS) == 0 ) 322 | break; 323 | 324 | if ( dpm.verbose ) 325 | dtxmsg_deb("magic bpt: %a, tid=%d\n", bpt, tid); 326 | 327 | if ( !dpm.handle_dtxmsg_bpt() ) 328 | break; 329 | 330 | // resume process 331 | request_continue_process(); 332 | run_requests(); 333 | } 334 | break; 335 | 336 | default: 337 | break; 338 | } 339 | 340 | return 0; 341 | } 342 | 343 | //----------------------------------------------------------------------------- 344 | void dtxmsg_plugmod_t::set_dtxmsg_bpt(ea_t ea) 345 | { 346 | if ( !add_bpt(ea, 1, BPT_DEFAULT) ) 347 | { 348 | dtxmsg_deb("Error: failed to add breakpoint at %a\n", ea); 349 | return; 350 | } 351 | dtxmsg_node.altset_ea(ea, 1, DTXMSG_ALT_BPTS); 352 | dtxmsg_deb("magic bpt: %a\n", ea); 353 | } 354 | 355 | //----------------------------------------------------------------------------- 356 | typedef janitor_t mbl_janitor_t; 357 | template <> inline mbl_janitor_t::~janitor_t() 358 | { 359 | delete resource; 360 | } 361 | 362 | //----------------------------------------------------------------------------- 363 | void dtxmsg_plugmod_t::set_dtxmsg_bpts_xcode8(void) 364 | { 365 | const char *method = "-[DTXMessageParser parseMessageWithExceptionHandler:]"; 366 | ea_t ea = get_name_ea(BADADDR, method); 367 | if ( ea == BADADDR ) 368 | { 369 | dtxmsg_deb("failed to find %s in the database\n", method); 370 | return; 371 | } 372 | 373 | func_t *pfn = get_func(ea); 374 | if ( pfn == NULL ) 375 | { 376 | dtxmsg_deb("Error: no function found at %a\n", ea); 377 | return; 378 | } 379 | 380 | mba_ranges_t mbr(pfn); 381 | hexrays_failure_t hf; 382 | mbl_array_t *mba = gen_microcode( 383 | mbr, 384 | &hf, 385 | NULL, 386 | DECOMP_NO_WAIT, 387 | MMAT_GLBOPT1); 388 | 389 | if ( mba == NULL ) 390 | { 391 | dtxmsg_deb("microcode failure at %a: %s\n", hf.errea, hf.desc().c_str()); 392 | return; 393 | } 394 | 395 | mbl_janitor_t mj(mba); 396 | 397 | // add breakpoints after calls to -[DTXMessageParser waitForMoreData:incrementalBuffer:]. 398 | // this function returns a pointer to the raw DTXMessage data. 399 | struct ida_local bpt_finder_t : public minsn_visitor_t 400 | { 401 | dtxmsg_plugmod_t *dpm; 402 | bpt_finder_t(dtxmsg_plugmod_t *_dpm) : dpm(_dpm) {} 403 | virtual int idaapi visit_minsn(void) 404 | { 405 | if ( curins->opcode == m_call ) 406 | { 407 | const mcallinfo_t *ci = curins->d.f; 408 | if ( ci->args.size() == 4 && ci->callee == get_name_ea(BADADDR, "_objc_msgSend") ) 409 | { 410 | const mcallarg_t &selarg = ci->args[1]; 411 | if ( selarg.t == mop_a || selarg.a->t == mop_v ) 412 | { 413 | qstring sel; 414 | ea_t selea = selarg.a->g; 415 | if ( is_strlit(get_flags(selea)) 416 | && get_strlit_contents(&sel, selea, -1, STRTYPE_C) > 0 417 | && sel == "waitForMoreData:incrementalBuffer:" 418 | // ignore calls with a constant as the length argument. they are likely just 419 | // reading the message header. we are only interested in calls that will return 420 | // a pointer to the full serialized message. 421 | && ci->args[2].t != mop_n ) 422 | { 423 | dpm->set_dtxmsg_bpt(get_item_end(curins->ea)); 424 | } 425 | } 426 | } 427 | } 428 | return 0; 429 | } 430 | }; 431 | 432 | bpt_finder_t bf(this); 433 | mba->for_all_insns(bf); 434 | } 435 | 436 | //----------------------------------------------------------------------------- 437 | static int find_retained_block(mbl_array_t *mba) 438 | { 439 | // find a variable that is initialized like: v1 = objc_retainBlock(&block); 440 | struct ida_local block_finder_t : public minsn_visitor_t 441 | { 442 | int idx; 443 | block_finder_t() : idx(-1) {} 444 | virtual int idaapi visit_minsn(void) 445 | { 446 | if ( curins->opcode == m_mov 447 | && curins->d.t == mop_l 448 | && curins->l.t == mop_d ) 449 | { 450 | const minsn_t *d = curins->l.d; 451 | if ( d->opcode == m_call ) 452 | { 453 | const mcallinfo_t *ci = d->d.f; 454 | if ( ci->callee == get_name_ea(BADADDR, "_objc_retainBlock") ) 455 | { 456 | // found a retained block 457 | idx = curins->d.l->idx; 458 | // if the code later assigns this variable to another one, 459 | // the new variable takes priority 460 | for ( minsn_t *n = curins->next; n != NULL; n = n->next ) 461 | { 462 | if ( n->opcode == m_mov 463 | && n->d.t == mop_l 464 | && n->l.t == mop_l 465 | && n->l.l->idx == idx ) 466 | { 467 | idx = n->d.l->idx; 468 | } 469 | } 470 | return 1; 471 | } 472 | } 473 | } 474 | return 0; 475 | } 476 | }; 477 | 478 | block_finder_t bf; 479 | mba->for_all_topinsns(bf); 480 | return bf.idx; 481 | } 482 | 483 | //----------------------------------------------------------------------------- 484 | void dtxmsg_plugmod_t::set_dtxmsg_bpts_xcode9(void) 485 | { 486 | const char *method = "-[DTXMessageParser parseIncomingBytes:length:]"; 487 | ea_t ea = get_name_ea(BADADDR, method); 488 | if ( ea == BADADDR ) 489 | { 490 | dtxmsg_deb("Error: failed to find %s in the database\n", method); 491 | return; 492 | } 493 | 494 | func_t *pfn = get_func(ea); 495 | if ( pfn == NULL ) 496 | { 497 | dtxmsg_deb("Error: no function found at %a\n", ea); 498 | return; 499 | } 500 | 501 | // in Xcode 9, -[DTXMessageParser parseMessageWithExceptionHandler:] was replaced 502 | // with a block function. 503 | func_t *parser_block = NULL; 504 | 505 | func_item_iterator_t fii; 506 | for ( bool ok = fii.set(pfn); ok; ok = fii.next_addr() ) 507 | { 508 | ea_t xref = get_first_dref_from(fii.current()); 509 | if ( xref != BADADDR ) 510 | { 511 | func_t *_pfn = get_func(xref); 512 | if ( _pfn != NULL && get_name(xref).find("block_invoke") != qstring::npos ) 513 | { 514 | parser_block = _pfn; 515 | break; 516 | } 517 | } 518 | } 519 | 520 | if ( parser_block == NULL ) 521 | { 522 | dtxmsg_deb("Error: expected a block function in %s\n", method); 523 | return; 524 | } 525 | 526 | mba_ranges_t mbr(parser_block); 527 | hexrays_failure_t hf; 528 | mbl_array_t *mba = gen_microcode( 529 | mbr, 530 | &hf, 531 | NULL, 532 | DECOMP_NO_WAIT, 533 | MMAT_LVARS); 534 | 535 | if ( mba == NULL ) 536 | { 537 | dtxmsg_deb("microcode failure at %a: %s\n", hf.errea, hf.desc().c_str()); 538 | return; 539 | } 540 | 541 | mbl_janitor_t mj(mba); 542 | 543 | // Xcode 9 also replaced -[DTXMessageParser waitForMoreData:incrementalBuffer:] 544 | // with a block function. the return value of this block function will be 545 | // a pointer to the serialized message data. we must find all instances where 546 | // it is invoked. 547 | int idx = find_retained_block(mba); 548 | if ( idx < 0 ) 549 | { 550 | dtxmsg_deb("Error: expected to find objc_retainBlock() in function %a\n", parser_block->start_ea); 551 | return; 552 | } 553 | 554 | struct ida_local bpt_finder_t : public minsn_visitor_t 555 | { 556 | dtxmsg_plugmod_t *dpm; 557 | int idx; 558 | bpt_finder_t(dtxmsg_plugmod_t *_dpm, int _idx) : dpm(_dpm), idx(_idx) {} 559 | virtual int idaapi visit_minsn(void) 560 | { 561 | if ( curins->opcode == m_icall ) 562 | { 563 | const mcallinfo_t *ci = curins->d.f; 564 | // if the block variable is the first argument for an indirect call, 565 | // this is likely a call to the invoke function 566 | if ( ci->args.size() >= 2 567 | && ci->args[0].t == mop_l 568 | && ci->args[0].l->idx == idx 569 | // ignore calls with a constant as the length argument. they are likely just 570 | // reading the message header. we are only interested in calls that will return 571 | // a pointer to the full serialized message 572 | && ci->args[1].t != mop_n ) 573 | { 574 | dpm->set_dtxmsg_bpt(get_item_end(curins->ea)); 575 | } 576 | } 577 | return 0; 578 | } 579 | }; 580 | 581 | bpt_finder_t bf(this, idx); 582 | mba->for_all_insns(bf); 583 | } 584 | 585 | //----------------------------------------------------------------------------- 586 | void dtxmsg_plugmod_t::print_node_info(const char *pfx) const 587 | { 588 | dtxmsg_deb("node info: %s\n", pfx); 589 | 590 | uint64 version = 0; 591 | dtxmsg_node.supval(DTXMSG_ALT_VERSION, &version, sizeof(version)); 592 | dtxmsg_deb(" dtx version: %llX\n", version); 593 | 594 | for ( nodeidx_t idx = dtxmsg_node.altfirst(DTXMSG_ALT_BPTS); 595 | idx != BADNODE; 596 | idx = dtxmsg_node.altnext(idx, DTXMSG_ALT_BPTS) ) 597 | { 598 | dtxmsg_deb(" magic bpt: %a\n", node2ea(idx)); 599 | } 600 | } 601 | 602 | //----------------------------------------------------------------------------- 603 | ssize_t idaapi idb_listener_t::on_event(ssize_t notification_code, va_list va) 604 | { 605 | switch ( notification_code ) 606 | { 607 | case idb_event::auto_empty_finally: 608 | { 609 | uint64 version = 0; 610 | dpm.dtxmsg_node.supval(DTXMSG_ALT_VERSION, &version, sizeof(version)); 611 | 612 | // sometime around Xcode 9, Apple decided to change everything 613 | if ( version < 0x40EED00000000000LL ) 614 | dpm.set_dtxmsg_bpts_xcode8(); 615 | else 616 | dpm.set_dtxmsg_bpts_xcode9(); 617 | 618 | dpm.run(0); 619 | } 620 | break; 621 | 622 | case idb_event::allsegs_moved: 623 | { 624 | const segm_move_infos_t *smi = va_arg(va, segm_move_infos_t *); 625 | 626 | for ( segm_move_infos_t::const_iterator i = smi->begin(); i != smi->end(); ++i ) 627 | { 628 | nodeidx_t n1 = ea2node(i->from); 629 | nodeidx_t n2 = ea2node(i->to); 630 | dpm.dtxmsg_node.altshift(n1, n2, i->size, DTXMSG_ALT_BPTS); 631 | } 632 | 633 | dpm.print_node_info("rebased"); 634 | } 635 | break; 636 | 637 | default: 638 | break; 639 | } 640 | 641 | return 0; 642 | } 643 | 644 | //----------------------------------------------------------------------------- 645 | ssize_t idaapi ui_listener_t::on_event(ssize_t code, va_list) 646 | { 647 | switch ( code ) 648 | { 649 | case ui_ready_to_run: 650 | { 651 | // it is possible that struct DTXMessageHeader was encoded in the objc types. 652 | // we overwrite it with our own struct that has better member names. 653 | static const char *decls = 654 | "struct DTXMessageHeader \n" 655 | "{ \n" 656 | " uint32_t magic; \n" 657 | " uint32_t cb; \n" 658 | " uint16_t fragmentId; \n" 659 | " uint16_t fragmentCount; \n" 660 | " uint32_t length; \n" 661 | " uint32_t identifier; \n" 662 | " uint32_t conversationIndex; \n" 663 | " uint32_t channelCode; \n" 664 | " uint32_t expectsReply; \n" 665 | "}; \n" 666 | "struct DTXMessagePayloadHeader \n" 667 | "{ \n" 668 | " uint32_t flags; \n" 669 | " uint32_t auxiliaryLength; \n" 670 | " uint64_t totalLength; \n" 671 | "}; \n"; 672 | 673 | if ( h2ti(NULL, NULL, decls, HTI_DCL, NULL, NULL, msg) != 0 674 | || import_type(NULL, -1, "DTXMessageHeader") == BADNODE 675 | || import_type(NULL, -1, "DTXMessagePayloadHeader") == BADNODE ) 676 | { 677 | dtxmsg_deb("Error: failed to import DTXMessage helper types\n"); 678 | } 679 | 680 | // try to run the plugin now. if we're loading a new file, 681 | // it is too early to run the plugin because breakpoints 682 | // have not been detected. but that's ok, we will try again 683 | // after analysis has finished. 684 | dpm.run(0); 685 | } 686 | break; 687 | 688 | default: 689 | break; 690 | } 691 | 692 | return 0; 693 | } 694 | 695 | //----------------------------------------------------------------------------- 696 | static plugmod_t *idaapi init(void) 697 | { 698 | ea_t version_ea = get_name_ea(BADADDR, "_DTXConnectionServicesVersionNumber"); 699 | if ( version_ea == BADADDR ) 700 | { 701 | dtxmsg_deb("input file does not look like the DTXConnectionServices library, skipping\n"); 702 | return nullptr; 703 | } 704 | 705 | if ( !init_hexrays_plugin() ) 706 | { 707 | dtxmsg_deb("Error: this plugin requires the hexrays decompiler!\n"); 708 | return nullptr; 709 | } 710 | 711 | // initialize the plugin context 712 | dtxmsg_plugmod_t *dpm = new dtxmsg_plugmod_t; 713 | 714 | if ( dpm->dtxmsg_node.supval(DTXMSG_ALT_VERSION, NULL, 0) == -1 ) 715 | { 716 | // working with a fresh database - must perform some setup 717 | uint64 version = get_qword(version_ea); 718 | dpm->dtxmsg_node.supset(DTXMSG_ALT_VERSION, &version, sizeof(version)); 719 | 720 | const char *dbgname = NULL; 721 | const char *exe = NULL; 722 | const char *lib = NULL; 723 | int port = -1; 724 | 725 | switch ( dpm->ph.id ) 726 | { 727 | case PLFM_ARM: 728 | dbgname = "ios"; 729 | exe = "/Developer/Library/PrivateFrameworks/DVTInstrumentsFoundation.framework/DTServiceHub"; 730 | lib = "/Developer/Library/PrivateFrameworks/DTXConnectionServices.framework/DTXConnectionServices"; 731 | port = 4321; // use ios_deploy to relay connection to the debugserver. see 'ios_deploy usbproxy -h'. 732 | break; 733 | 734 | case PLFM_386: 735 | dbgname = "mac"; 736 | exe = "/Applications/Xcode.app/Contents/MacOS/Xcode"; 737 | lib = "/Applications/Xcode.app/Contents/SharedFrameworks/DTXConnectionServices.framework/Versions/A/DTXConnectionServices"; 738 | port = 23946; 739 | break; 740 | 741 | default: 742 | dtxmsg_deb("Error: unsupported architecture: %d\n", dpm->ph.id); 743 | delete dpm; 744 | return nullptr; 745 | } 746 | 747 | if ( !load_debugger(dbgname, true) ) 748 | { 749 | dtxmsg_deb("Error: failed to load %s debugger module\n", dbgname); 750 | delete dpm; 751 | return nullptr; 752 | } 753 | 754 | set_process_options(exe, NULL, NULL, "localhost", NULL, port); 755 | set_root_filename(lib); 756 | } 757 | else 758 | { 759 | // already configured the debugging environment 760 | dpm->print_node_info("saved"); 761 | } 762 | 763 | // parse command line options 764 | qstring opts = get_plugin_options("dtxmsg"); 765 | if ( !opts.empty() ) 766 | { 767 | char *ctx = NULL; 768 | for ( char *tok = qstrtok(opts.begin(), ":", &ctx); 769 | tok != NULL; 770 | tok = qstrtok(NULL, ":", &ctx) ) 771 | { 772 | pid_t _pid = strtol(tok, NULL, 0); 773 | if ( _pid != 0 ) 774 | { 775 | // process ID 776 | dpm->pid = _pid; 777 | continue; 778 | } 779 | else if ( qisabspath(tok) ) 780 | { 781 | // logging directory 782 | qstrncpy(dpm->logdir, tok, sizeof(dpm->logdir)); 783 | continue; 784 | } 785 | else if ( qstrlen(tok) == 1 ) 786 | { 787 | // verbose mode 788 | switch ( tok[0] ) 789 | { 790 | case 'v': 791 | case 'V': 792 | dpm->verbose = true; 793 | continue; 794 | default: 795 | break; 796 | } 797 | } 798 | dtxmsg_deb("Warning: bad command line arg: %s\n", tok); 799 | } 800 | } 801 | 802 | if ( qstrlen(dpm->logdir) == 0 ) 803 | qtmpnam(dpm->logdir, sizeof(dpm->logdir)); 804 | 805 | if ( qmkdir(dpm->logdir, 0755) != 0 && errno != EEXIST ) 806 | { 807 | dtxmsg_deb("Error: failed to mkdir %s: %s\n", dpm->logdir, winerr(errno)); 808 | delete dpm; 809 | return nullptr; 810 | } 811 | 812 | char path[QMAXPATH]; 813 | qmakepath(path, sizeof(path), dpm->logdir, "headers.log", NULL); 814 | 815 | dpm->headers_fp = qfopen(path, "w"); 816 | if ( dpm->headers_fp == NULL ) 817 | { 818 | dtxmsg_deb("Error: failed to open %s: %s\n", path, winerr(errno)); 819 | delete dpm; 820 | return nullptr; 821 | } 822 | 823 | dtxmsg_deb("logging header info to: %s\n", path); 824 | 825 | hook_event_listener(HT_IDB, &dpm->idb_listener, dpm); 826 | hook_event_listener(HT_DBG, &dpm->dbg_listener, dpm); 827 | hook_event_listener(HT_UI, &dpm->ui_listener, dpm); 828 | 829 | return dpm; 830 | } 831 | 832 | //----------------------------------------------------------------------------- 833 | dtxmsg_plugmod_t::dtxmsg_plugmod_t(void) : ph(PH) 834 | { 835 | dtxmsg_node.create(DTXMSG_NODE); 836 | } 837 | 838 | //----------------------------------------------------------------------------- 839 | dtxmsg_plugmod_t::~dtxmsg_plugmod_t(void) 840 | { 841 | if ( headers_fp != NULL ) 842 | qfclose(headers_fp); 843 | 844 | term_hexrays_plugin(); 845 | } 846 | 847 | //----------------------------------------------------------------------------- 848 | bool idaapi dtxmsg_plugmod_t::run(size_t) 849 | { 850 | if ( dtxmsg_node.altfirst(DTXMSG_ALT_BPTS) == BADNODE ) 851 | { 852 | dtxmsg_deb("no breakpoints detected, cannot run yet\n"); 853 | return false; 854 | } 855 | 856 | if ( pid == 0 ) 857 | { 858 | dtxmsg_deb("No PID specified. You must attach manually or use -Odtxmsg:PID.\n"); 859 | return false; 860 | } 861 | 862 | int res = attach_process(pid, -1); 863 | if ( res != 1 ) 864 | { 865 | dtxmsg_deb("Error: failed to attach to PID %d. errcode=%d\n", pid, res); 866 | return false; 867 | } 868 | 869 | res = wait_for_next_event(WFNE_SUSP|WFNE_SILENT, -1); 870 | 871 | if ( res != PROCESS_ATTACHED 872 | && res != PROCESS_SUSPENDED ) 873 | { 874 | dtxmsg_deb("Error: unexpected debugger event: %d\n", res); 875 | return false; 876 | } 877 | 878 | dtxmsg_deb("successfully attached to PID %d\n", pid); 879 | 880 | request_continue_process(); 881 | run_requests(); 882 | 883 | return true; 884 | } 885 | 886 | //----------------------------------------------------------------------------- 887 | plugin_t PLUGIN = 888 | { 889 | IDP_INTERFACE_VERSION, 890 | PLUGIN_HIDE|PLUGIN_MULTI, // plugin flags 891 | init, // initialize 892 | NULL, // terminate. this pointer may be NULL. 893 | NULL, // invoke plugin 894 | NULL, // long comment about the plugin 895 | NULL, // multiline help about the plugin 896 | "", // the preferred short name of the plugin 897 | NULL // the preferred hotkey to run the plugin 898 | }; 899 | -------------------------------------------------------------------------------- /dtxmsg.h: -------------------------------------------------------------------------------- 1 | #ifndef DTXMSG_H 2 | #define DTXMSG_H 3 | 4 | #include "dtxmsg_common.h" 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #define DTXMSG_NODE "$ dtxmsg" // stores internal plugin data 12 | #define DTXMSG_ALT_VERSION 0 // supval: DTXConnectionServices library version 13 | #define DTXMSG_ALT_BPTS 'B' // altval_ea: array of bpt eas for inspecting incoming packets 14 | 15 | #define DTXMSG_DEB_PFX "DTXMSG: " 16 | 17 | struct dtxmsg_plugmod_t; 18 | 19 | //-------------------------------------------------------------------------- 20 | DECLARE_LISTENER(idb_listener_t, struct dtxmsg_plugmod_t, dpm); 21 | DECLARE_LISTENER(dbg_listener_t, struct dtxmsg_plugmod_t, dpm); 22 | DECLARE_LISTENER(ui_listener_t, struct dtxmsg_plugmod_t, dpm); 23 | 24 | //-------------------------------------------------------------------------- 25 | struct dtxmsg_plugmod_t : public plugmod_t 26 | { 27 | // processor info for the current idb 28 | processor_t &ph; 29 | 30 | // persist important configuration details in the database 31 | netnode dtxmsg_node; 32 | 33 | // directory where captured + decoded messages are stored 34 | char logdir[QMAXPATH] = { 0 }; 35 | 36 | // log the header of each captured message in this file 37 | FILE *headers_fp = NULL; 38 | 39 | // print extra messages to the output window 40 | bool verbose = false; 41 | 42 | // pid of the debugged process (either DTServiceHub or Xcode) 43 | pid_t pid = 0; 44 | 45 | // event listeners 46 | idb_listener_t idb_listener = idb_listener_t(*this); 47 | dbg_listener_t dbg_listener = dbg_listener_t(*this); 48 | ui_listener_t ui_listener = ui_listener_t(*this); 49 | 50 | dtxmsg_plugmod_t(void); 51 | ~dtxmsg_plugmod_t(void); 52 | 53 | // attach to the target process and wait for breakpoint events 54 | virtual bool idaapi run(size_t code) override; 55 | 56 | // handle a breakpoint event 57 | bool handle_dtxmsg_bpt(void) const; 58 | 59 | // set a breakpoint in the target process 60 | void set_dtxmsg_bpt(ea_t ea); 61 | 62 | // detect breakpoint locations 63 | void set_dtxmsg_bpts_xcode8(void); 64 | void set_dtxmsg_bpts_xcode9(void); 65 | 66 | // print plugin configuration to output window 67 | void print_node_info(const char *pfx) const; 68 | 69 | // parse a message in memory 70 | bool parse_message(ea_t buf, const DTXMessageHeader &mheader) const; 71 | 72 | // deserialize the payload stored at the given path 73 | bool deserialize_payload(const char *path, uint32 id) const; 74 | 75 | // try to parse a serialized object and print it to a file in plain text 76 | bool deserialize_component( 77 | const char *label, 78 | uint32 identifier, 79 | FILE *payload_fp, 80 | uint64 off, 81 | uint64 length, 82 | bool is_array) const; 83 | }; 84 | 85 | //----------------------------------------------------------------------------- 86 | THREAD_SAFE AS_PRINTF(1, 0) void dtxmsg_vdeb(const char *format, va_list va) 87 | { 88 | qstring buf(DTXMSG_DEB_PFX); 89 | buf.cat_vsprnt(format, va); 90 | msg("%s", buf.c_str()); 91 | } 92 | 93 | //----------------------------------------------------------------------------- 94 | THREAD_SAFE AS_PRINTF(1, 2) void dtxmsg_deb(const char *format, ...) 95 | { 96 | va_list va; 97 | va_start(va, format); 98 | dtxmsg_vdeb(format, va); 99 | va_end(va); 100 | } 101 | 102 | #endif // DTXMSG_H 103 | -------------------------------------------------------------------------------- /dtxmsg_client.cpp: -------------------------------------------------------------------------------- 1 | #include "dtxmsg_client.h" 2 | #include 3 | #include 4 | 5 | static qstring device_id; // user's preferred device (-d option) 6 | static bool found_device = false; // has the user's preferred device been detected? 7 | static bool verbose = false; // verbose mode (-v option) 8 | static int cur_message = 0; // current message id 9 | static int cur_channel = 0; // current channel id 10 | static CFDictionaryRef channels = NULL; // list of available channels published by the instruments server 11 | static int pid2kill = -1; // process id to kill ("kill" option) 12 | static const char *bid2launch = NULL; // bundle id of app to launch ("launch" option) 13 | static bool proclist = false; // print the process list ("proclist" option) 14 | static bool applist = false; // print the application list ("applist" option) 15 | 16 | //----------------------------------------------------------------------------- 17 | // pointers to functions in MobileDevice.framework 18 | static mach_error_t (*AMDeviceNotificationSubscribe)(am_device_notification_callback_t *, int, int, void *, am_device_notification **); 19 | static mach_error_t (*AMDeviceNotificationUnsubscribe)(am_device_notification *); 20 | static CFStringRef (*AMDeviceCopyDeviceIdentifier)(am_device *); 21 | static mach_error_t (*AMDeviceConnect)(am_device *); 22 | static int (*AMDeviceIsPaired)(am_device *); 23 | static mach_error_t (*AMDeviceValidatePairing)(am_device *); 24 | static mach_error_t (*AMDeviceStartSession)(am_device *); 25 | static mach_error_t (*AMDeviceStopSession)(am_device *); 26 | static mach_error_t (*AMDeviceDisconnect)(am_device *); 27 | static mach_error_t (*AMDeviceSecureStartService)(am_device *, CFStringRef, int *, am_device_service_connection **); 28 | static void (*AMDServiceConnectionInvalidate)(am_device_service_connection *); 29 | static mach_error_t (*AMDServiceConnectionSend)(am_device_service_connection *, const void *, size_t); 30 | static mach_error_t (*AMDServiceConnectionReceive)(am_device_service_connection *, void *, size_t); 31 | 32 | //----------------------------------------------------------------------------- 33 | static bool load_mobile_device(void) 34 | { 35 | const char *path = "/System/Library/PrivateFrameworks/MobileDevice.framework/MobileDevice"; 36 | void *handle = dlopen(path, RTLD_NOW); 37 | if ( handle == NULL ) 38 | { 39 | qeprintf("dlopen() failed for %s: %s\n", path, dlerror()); 40 | return false; 41 | } 42 | 43 | #define BINDFUN(name, type) \ 44 | name = reinterpret_cast(dlsym(handle, #name)); \ 45 | if ( name == NULL ) \ 46 | { \ 47 | qeprintf("Could not find function " #name " in %s\n", path); \ 48 | return false; \ 49 | } 50 | 51 | BINDFUN(AMDeviceNotificationSubscribe, mach_error_t (*)(am_device_notification_callback_t *, int, int, void *, am_device_notification **)); 52 | BINDFUN(AMDeviceNotificationUnsubscribe, mach_error_t (*)(am_device_notification *)); 53 | BINDFUN(AMDeviceCopyDeviceIdentifier, CFStringRef (*)(am_device *)); 54 | BINDFUN(AMDeviceConnect, mach_error_t (*)(am_device *)); 55 | BINDFUN(AMDeviceIsPaired, int (*)(am_device *)); 56 | BINDFUN(AMDeviceValidatePairing, mach_error_t (*)(am_device *)); 57 | BINDFUN(AMDeviceStartSession, mach_error_t (*)(am_device *)); 58 | BINDFUN(AMDeviceStopSession, mach_error_t (*)(am_device *)); 59 | BINDFUN(AMDeviceDisconnect, mach_error_t (*)(am_device *)); 60 | BINDFUN(AMDeviceSecureStartService, mach_error_t (*)(am_device *, CFStringRef, int *, am_device_service_connection **)); 61 | BINDFUN(AMDServiceConnectionInvalidate, void (*)(am_device_service_connection *)); 62 | BINDFUN(AMDServiceConnectionSend, mach_error_t (*)(am_device_service_connection *, const void *, size_t)); 63 | BINDFUN(AMDServiceConnectionReceive, mach_error_t (*)(am_device_service_connection *, void *, size_t)); 64 | 65 | #undef BINDFUN 66 | 67 | return true; 68 | } 69 | 70 | //----------------------------------------------------------------------------- 71 | // callback that handles device notifications. called once for each connected device. 72 | static void device_callback(am_device_notification_callback_info *cbi, void *arg) 73 | { 74 | if ( cbi->code != ADNCI_MSG_CONNECTED ) 75 | return; 76 | 77 | CFStringRef id = AMDeviceCopyDeviceIdentifier(cbi->dev); 78 | qstring _device_id = to_qstring(id); 79 | CFRelease(id); 80 | 81 | if ( !device_id.empty() && device_id != _device_id ) 82 | return; 83 | 84 | found_device = true; 85 | 86 | if ( verbose ) 87 | qprintf("found device: %s\n", _device_id.c_str()); 88 | 89 | do 90 | { 91 | // start a session on the device 92 | if ( AMDeviceConnect(cbi->dev) != kAMDSuccess 93 | || AMDeviceIsPaired(cbi->dev) == 0 94 | || AMDeviceValidatePairing(cbi->dev) != kAMDSuccess 95 | || AMDeviceStartSession(cbi->dev) != kAMDSuccess ) 96 | { 97 | qeprintf("Error: failed to start a session on the device\n"); 98 | break; 99 | } 100 | 101 | am_device_service_connection **connptr = (am_device_service_connection **)arg; 102 | 103 | // launch the instruments server 104 | mach_error_t err = AMDeviceSecureStartService( 105 | cbi->dev, 106 | CFSTR("com.apple.instruments.remoteserver"), 107 | NULL, 108 | connptr); 109 | 110 | if ( err != kAMDSuccess ) 111 | { 112 | qeprintf("Failed to start the instruments server (0x%x). " 113 | "Perhaps DeveloperDiskImage.dmg is not installed on the device?\n", err); 114 | break; 115 | } 116 | 117 | if ( verbose ) 118 | qprintf("successfully launched instruments server\n"); 119 | } 120 | while ( false ); 121 | 122 | AMDeviceStopSession(cbi->dev); 123 | AMDeviceDisconnect(cbi->dev); 124 | 125 | CFRunLoopStop(CFRunLoopGetCurrent()); 126 | } 127 | 128 | //----------------------------------------------------------------------------- 129 | // launch the instruments server on the user's device. 130 | // returns a handle that can be used to send/receive data to/from the server. 131 | static am_device_service_connection *start_server(void) 132 | { 133 | am_device_notification *notify_handle = NULL; 134 | am_device_service_connection *conn = NULL; 135 | 136 | mach_error_t err = AMDeviceNotificationSubscribe( 137 | device_callback, 138 | 0, 139 | 0, 140 | &conn, 141 | ¬ify_handle); 142 | 143 | if ( err != kAMDSuccess ) 144 | { 145 | qeprintf("failed to register device notifier: 0x%x\n", err); 146 | return NULL; 147 | } 148 | 149 | // start a run loop, and wait for the device notifier to call our callback function. 150 | // if no device was detected within 3 seconds, we bail out. 151 | CFRunLoopRunInMode(kCFRunLoopDefaultMode, 3, false); 152 | 153 | AMDeviceNotificationUnsubscribe(notify_handle); 154 | 155 | if ( conn == NULL && !found_device ) 156 | { 157 | if ( device_id.empty() ) 158 | qeprintf("Failed to find a connected device\n"); 159 | else 160 | qeprintf("Failed to find device with id = %s\n", device_id.c_str()); 161 | return NULL; 162 | } 163 | 164 | return conn; 165 | } 166 | 167 | //----------------------------------------------------------------------------- 168 | // "call" an Objective-C method in the instruments server process 169 | // conn server handle 170 | // channel determines the object that will receive the message, 171 | // obtained by a previous call to make_channel() 172 | // selector method name 173 | // args serialized list of arguments for the method 174 | // expects_reply do we expect a return value from the method? 175 | // the return value can be obtained by a subsequent call to recv_message() 176 | static bool send_message( 177 | am_device_service_connection *conn, 178 | int channel, 179 | CFStringRef selector, 180 | const message_aux_t *args, 181 | bool expects_reply = true) 182 | { 183 | uint32 id = ++cur_message; 184 | 185 | bytevec_t aux; 186 | if ( args != NULL ) 187 | args->get_bytes(&aux); 188 | 189 | bytevec_t sel; 190 | if ( selector != NULL ) 191 | archive(&sel, selector); 192 | 193 | DTXMessagePayloadHeader pheader; 194 | // the low byte of the payload flags represents the message type. 195 | // so far it seems that all requests to the instruments server have message type 2. 196 | pheader.flags = 0x2 | (expects_reply ? 0x1000 : 0); 197 | pheader.auxiliaryLength = aux.size(); 198 | pheader.totalLength = aux.size() + sel.size(); 199 | 200 | DTXMessageHeader mheader; 201 | mheader.magic = 0x1F3D5B79; 202 | mheader.cb = sizeof(DTXMessageHeader); 203 | mheader.fragmentId = 0; 204 | mheader.fragmentCount = 1; 205 | mheader.length = sizeof(pheader) + pheader.totalLength; 206 | mheader.identifier = id; 207 | mheader.conversationIndex = 0; 208 | mheader.channelCode = channel; 209 | mheader.expectsReply = (expects_reply ? 1 : 0); 210 | 211 | bytevec_t msg; 212 | append_v(msg, &mheader, sizeof(mheader)); 213 | append_v(msg, &pheader, sizeof(pheader)); 214 | append_b(msg, aux); 215 | append_b(msg, sel); 216 | 217 | size_t msglen = msg.size(); 218 | 219 | if ( AMDServiceConnectionSend(conn, msg.begin(), msglen) != msglen ) 220 | { 221 | qeprintf("Failed to send 0x%x bytes of message: %s\n", msglen, winerr(errno)); 222 | return false; 223 | } 224 | 225 | return true; 226 | } 227 | 228 | //----------------------------------------------------------------------------- 229 | // handle a response from the server. 230 | // conn server handle 231 | // retobj contains the return value for the method invoked by send_message() 232 | // aux usually empty, except in specific situations (see _notifyOfPublishedCapabilities) 233 | static bool recv_message( 234 | am_device_service_connection *conn, 235 | CFTypeRef *retobj, 236 | CFArrayRef *aux) 237 | { 238 | uint32 id = 0; 239 | bytevec_t payload; 240 | 241 | while ( true ) 242 | { 243 | DTXMessageHeader mheader; 244 | ssize_t nrecv = AMDServiceConnectionReceive(conn, &mheader, sizeof(mheader)); 245 | if ( nrecv != sizeof(mheader) ) 246 | { 247 | qeprintf("failed to read message header: %s, nrecv = %lx\n", winerr(errno), nrecv); 248 | return false; 249 | } 250 | 251 | if ( mheader.magic != 0x1F3D5B79 ) 252 | { 253 | qeprintf("bad header magic: %x\n", mheader.magic); 254 | return false; 255 | } 256 | 257 | if ( mheader.conversationIndex == 1 ) 258 | { 259 | // the message is a response to a previous request, so it should have the same id as the request 260 | if ( mheader.identifier != cur_message ) 261 | { 262 | qeprintf("expected response to message id=%d, got a new message with id=%d\n", cur_message, mheader.identifier); 263 | return false; 264 | } 265 | } 266 | else if ( mheader.conversationIndex == 0 ) 267 | { 268 | // the message is not a response to a previous request. in this case, different iOS versions produce different results. 269 | // on iOS 9, the incoming message can have the same message ID has the previous message we sent to the server. 270 | // on later versions, the incoming message will have a new message ID. we must be aware of both situations. 271 | if ( mheader.identifier > cur_message ) 272 | { 273 | // new message id, we must update the count on our side 274 | cur_message = mheader.identifier; 275 | } 276 | else if ( mheader.identifier < cur_message ) 277 | { 278 | // the id must match the previous request, anything else doesn't really make sense 279 | qeprintf("unexpected message ID: %d\n", mheader.identifier); 280 | return false; 281 | } 282 | } 283 | else 284 | { 285 | qeprintf("invalid conversation index: %d\n", mheader.conversationIndex); 286 | return false; 287 | } 288 | 289 | if ( mheader.fragmentId == 0 ) 290 | { 291 | id = mheader.identifier; 292 | // when reading multiple message fragments, the 0th fragment contains only a message header 293 | if ( mheader.fragmentCount > 1 ) 294 | continue; 295 | } 296 | 297 | // read the entire payload in the current fragment 298 | bytevec_t frag; 299 | frag.append(&mheader, sizeof(mheader)); 300 | frag.growfill(mheader.length); 301 | 302 | uchar *data = frag.begin() + sizeof(mheader); 303 | 304 | uint32 nbytes = 0; 305 | while ( nbytes < mheader.length ) 306 | { 307 | nrecv = AMDServiceConnectionReceive(conn, data+nbytes, mheader.length-nbytes); 308 | if ( nrecv <= 0 ) 309 | { 310 | qeprintf("failed reading from socket: %s\n", winerr(errno)); 311 | return false; 312 | } 313 | nbytes += nrecv; 314 | } 315 | 316 | // append to the incremental payload 317 | payload.append(data, mheader.length); 318 | 319 | // done reading message fragments? 320 | if ( mheader.fragmentId == mheader.fragmentCount - 1 ) 321 | break; 322 | } 323 | 324 | const DTXMessagePayloadHeader *pheader = (const DTXMessagePayloadHeader *)payload.begin(); 325 | 326 | // we don't know how to decompress messages yet 327 | uint8 compression = (pheader->flags & 0xFF000) >> 12; 328 | if ( compression != 0 ) 329 | { 330 | qeprintf("message is compressed (compression type %d)\n", compression); 331 | return false; 332 | } 333 | 334 | // serialized object array is located just after payload header 335 | const uchar *auxptr = payload.begin() + sizeof(DTXMessagePayloadHeader); 336 | uint32 auxlen = pheader->auxiliaryLength; 337 | 338 | // archived payload object appears after the auxiliary array 339 | const uchar *objptr = auxptr + auxlen; 340 | uint64 objlen = pheader->totalLength - auxlen; 341 | 342 | if ( auxlen != 0 && aux != NULL ) 343 | { 344 | qstring errbuf; 345 | CFArrayRef _aux = deserialize(auxptr, auxlen, &errbuf); 346 | if ( _aux == NULL ) 347 | { 348 | qeprintf("Error: %s\n", errbuf.c_str()); 349 | return false; 350 | } 351 | *aux = _aux; 352 | } 353 | 354 | if ( objlen != 0 && retobj != NULL ) 355 | *retobj = unarchive(objptr, objlen); 356 | 357 | return true; 358 | } 359 | 360 | //----------------------------------------------------------------------------- 361 | // perform the initial client-server handshake. 362 | // here we retrieve the list of available channels published by the instruments server. 363 | // we can open a given channel with make_channel(). 364 | static bool perform_handshake(am_device_service_connection *conn) 365 | { 366 | // I'm not sure if this argument is necessary - but Xcode uses it, so I'm using it too. 367 | CFMutableDictionaryRef capabilities = CFDictionaryCreateMutable(NULL, 0, NULL, NULL); 368 | 369 | int64 _v1 = 1; 370 | int64 _v2 = 2; 371 | 372 | CFNumberRef v1 = CFNumberCreate(NULL, kCFNumberSInt64Type, &_v1); 373 | CFNumberRef v2 = CFNumberCreate(NULL, kCFNumberSInt64Type, &_v2); 374 | 375 | CFDictionaryAddValue(capabilities, CFSTR("com.apple.private.DTXBlockCompression"), v2); 376 | CFDictionaryAddValue(capabilities, CFSTR("com.apple.private.DTXConnection"), v1); 377 | 378 | // serialize the dictionary 379 | message_aux_t args; 380 | args.append_obj(capabilities); 381 | 382 | CFRelease(capabilities); 383 | CFRelease(v1); 384 | CFRelease(v2); 385 | 386 | if ( !send_message(conn, 0, CFSTR("_notifyOfPublishedCapabilities:"), &args, false) ) 387 | return false; 388 | 389 | CFTypeRef obj = NULL; 390 | CFArrayRef aux = NULL; 391 | 392 | // we are now expecting the server to reply with the same message. 393 | // a description of all available channels will be provided in the arguments list. 394 | if ( !recv_message(conn, &obj, &aux) || obj == NULL || aux == NULL ) 395 | { 396 | qeprintf("Error: failed to receive response from _notifyOfPublishedCapabilities:\n"); 397 | return false; 398 | } 399 | 400 | bool ok = false; 401 | do 402 | { 403 | if ( CFGetTypeID(obj) != CFStringGetTypeID() 404 | || to_qstring((CFStringRef)obj) != "_notifyOfPublishedCapabilities:" ) 405 | { 406 | qeprintf("Error: unexpected message selector: %s\n", get_description(obj).c_str()); 407 | break; 408 | } 409 | 410 | CFDictionaryRef _channels; 411 | 412 | // extract the channel list from the arguments 413 | if ( CFArrayGetCount(aux) != 1 414 | || (_channels = (CFDictionaryRef)CFArrayGetValueAtIndex(aux, 0)) == NULL 415 | || CFGetTypeID(_channels) != CFDictionaryGetTypeID() 416 | || CFDictionaryGetCount(_channels) == 0 ) 417 | { 418 | qeprintf("channel list has an unexpected format:\n%s\n", get_description(aux).c_str()); 419 | break; 420 | } 421 | 422 | channels = (CFDictionaryRef)CFRetain(_channels); 423 | 424 | if ( verbose ) 425 | qprintf("channel list:\n%s\n", get_description(channels).c_str()); 426 | 427 | ok = true; 428 | } 429 | while ( false ); 430 | 431 | CFRelease(obj); 432 | CFRelease(aux); 433 | 434 | return ok; 435 | } 436 | 437 | //----------------------------------------------------------------------------- 438 | // establish a connection to a service in the instruments server process. 439 | // the channel identifier should be in the list of channels returned by the server 440 | // in perform_handshake(). after a channel is established, you can use send_message() 441 | // to remotely invoke Objective-C methods. 442 | static int make_channel(am_device_service_connection *conn, CFStringRef identifier) 443 | { 444 | if ( !CFDictionaryContainsKey(channels, identifier) ) 445 | { 446 | qeprintf("channel %s is not supported by the server\n", to_qstring(identifier).c_str()); 447 | return -1; 448 | } 449 | 450 | int code = ++cur_channel; 451 | 452 | message_aux_t args; 453 | args.append_int(code); 454 | args.append_obj(identifier); 455 | 456 | CFTypeRef retobj = NULL; 457 | 458 | // request to open the channel, expect an empty reply 459 | if ( !send_message(conn, 0, CFSTR("_requestChannelWithCode:identifier:"), &args) 460 | || !recv_message(conn, &retobj, NULL) ) 461 | { 462 | return -1; 463 | } 464 | 465 | if ( retobj != NULL ) 466 | { 467 | qeprintf("Error: _requestChannelWithCode:identifier: returned %s\n", get_description(retobj).c_str()); 468 | CFRelease(retobj); 469 | return -1; 470 | } 471 | 472 | return code; 473 | } 474 | 475 | //----------------------------------------------------------------------------- 476 | // invoke method -[DTDeviceInfoService runningProcesses] 477 | // args: none 478 | // returns: CFArrayRef procs 479 | static bool print_proclist(am_device_service_connection *conn) 480 | { 481 | int channel = make_channel(conn, CFSTR("com.apple.instruments.server.services.deviceinfo")); 482 | if ( channel < 0 ) 483 | return false; 484 | 485 | CFTypeRef retobj = NULL; 486 | 487 | if ( !send_message(conn, channel, CFSTR("runningProcesses"), NULL) 488 | || !recv_message(conn, &retobj, NULL) 489 | || retobj == NULL ) 490 | { 491 | qeprintf("Error: failed to retrieve return value for runningProcesses\n"); 492 | return false; 493 | } 494 | 495 | bool ok = true; 496 | if ( CFGetTypeID(retobj) == CFArrayGetTypeID() ) 497 | { 498 | CFArrayRef array = (CFArrayRef)retobj; 499 | 500 | qprintf("proclist:\n"); 501 | for ( size_t i = 0, size = CFArrayGetCount(array); i < size; i++ ) 502 | { 503 | CFDictionaryRef dict = (CFDictionaryRef)CFArrayGetValueAtIndex(array, i); 504 | 505 | CFStringRef _name = (CFStringRef)CFDictionaryGetValue(dict, CFSTR("name")); 506 | qstring name = to_qstring(_name); 507 | 508 | CFNumberRef _pid = (CFNumberRef)CFDictionaryGetValue(dict, CFSTR("pid")); 509 | int pid = 0; 510 | CFNumberGetValue(_pid, kCFNumberSInt32Type, &pid); 511 | 512 | qprintf("%6d %s\n", pid, name.c_str()); 513 | } 514 | } 515 | else 516 | { 517 | qeprintf("Error: process list is not in the expected format: %s\n", get_description(retobj).c_str()); 518 | ok = false; 519 | } 520 | 521 | CFRelease(retobj); 522 | return ok; 523 | } 524 | 525 | //----------------------------------------------------------------------------- 526 | // invoke method -[DTApplicationListingService installedApplicationsMatching:registerUpdateToken:] 527 | // args: CFDictionaryRef dict 528 | // CFStringRef token 529 | // returns CFArrayRef apps 530 | static bool print_applist(am_device_service_connection *conn) 531 | { 532 | int channel = make_channel(conn, CFSTR("com.apple.instruments.server.services.device.applictionListing")); 533 | if ( channel < 0 ) 534 | return false; 535 | 536 | // the method expects a dictionary and a string argument. 537 | // pass empty values so we get descriptions for all known applications. 538 | CFDictionaryRef dict = CFDictionaryCreate(NULL, NULL, NULL, 0, NULL, NULL); 539 | 540 | message_aux_t args; 541 | args.append_obj(dict); 542 | args.append_obj(CFSTR("")); 543 | 544 | CFRelease(dict); 545 | 546 | CFTypeRef retobj = NULL; 547 | 548 | if ( !send_message(conn, channel, CFSTR("installedApplicationsMatching:registerUpdateToken:"), &args) 549 | || !recv_message(conn, &retobj, NULL) 550 | || retobj == NULL ) 551 | { 552 | qeprintf("Error: failed to retrieve applist\n"); 553 | return false; 554 | } 555 | 556 | bool ok = true; 557 | if ( CFGetTypeID(retobj) == CFArrayGetTypeID() ) 558 | { 559 | CFArrayRef array = (CFArrayRef)retobj; 560 | for ( size_t i = 0, size = CFArrayGetCount(array); i < size; i++ ) 561 | { 562 | CFDictionaryRef app_desc = (CFDictionaryRef)CFArrayGetValueAtIndex(array, i); 563 | qprintf("%s\n", get_description(app_desc).c_str()); 564 | } 565 | } 566 | else 567 | { 568 | qeprintf("apps list has an unexpected format: %s\n", get_description(retobj).c_str()); 569 | ok = false; 570 | } 571 | 572 | CFRelease(retobj); 573 | return ok; 574 | } 575 | 576 | //----------------------------------------------------------------------------- 577 | // invoke method -[DTProcessControlService killPid:] 578 | // args: CFNumberRef process_id 579 | // returns: void 580 | static bool kill(am_device_service_connection *conn, int pid) 581 | { 582 | int channel = make_channel(conn, CFSTR("com.apple.instruments.server.services.processcontrol")); 583 | if ( channel < 0 ) 584 | return false; 585 | 586 | CFNumberRef _pid = CFNumberCreate(NULL, kCFNumberSInt32Type, &pid); 587 | 588 | message_aux_t args; 589 | args.append_obj(_pid); 590 | 591 | CFRelease(_pid); 592 | 593 | return send_message(conn, channel, CFSTR("killPid:"), &args, false); 594 | } 595 | 596 | //----------------------------------------------------------------------------- 597 | // invoke method -[DTProcessControlService launchSuspendedProcessWithDevicePath:bundleIdentifier:environment:arguments:] 598 | // args: CFStringRef app_path 599 | // CFStringRef bundle_id 600 | // CFArrayRef args_for_app 601 | // CFDictionaryRef environment_vars 602 | // CFDictionaryRef launch_options 603 | // returns: CFNumberRef pid 604 | static bool launch(am_device_service_connection *conn, const char *_bid) 605 | { 606 | int channel = make_channel(conn, CFSTR("com.apple.instruments.server.services.processcontrol")); 607 | if ( channel < 0 ) 608 | return false; 609 | 610 | // app path: not used, just pass empty string 611 | CFStringRef path = CFStringCreateWithCString(NULL, "", kCFStringEncodingUTF8); 612 | // bundle id 613 | CFStringRef bid = CFStringCreateWithCString(NULL, _bid, kCFStringEncodingUTF8); 614 | // args for app: not used, just pass empty array 615 | CFArrayRef appargs = CFArrayCreate(NULL, NULL, 0, NULL); 616 | // environment variables: not used, just pass empty dictionary 617 | CFDictionaryRef env = CFDictionaryCreate(NULL, NULL, NULL, 0, NULL, NULL); 618 | 619 | // launch options 620 | int _v0 = 0; // don't suspend the process after starting it 621 | int _v1 = 1; // kill the application if it is already running 622 | 623 | CFNumberRef v0 = CFNumberCreate(NULL, kCFNumberSInt32Type, &_v0); 624 | CFNumberRef v1 = CFNumberCreate(NULL, kCFNumberSInt32Type, &_v1); 625 | 626 | const void *keys[] = 627 | { 628 | CFSTR("StartSuspendedKey"), 629 | CFSTR("KillExisting") 630 | }; 631 | const void *values[] = { v0, v1 }; 632 | CFDictionaryRef options = CFDictionaryCreate( 633 | NULL, 634 | keys, 635 | values, 636 | qnumber(values), 637 | NULL, 638 | NULL); 639 | 640 | message_aux_t args; 641 | args.append_obj(path); 642 | args.append_obj(bid); 643 | args.append_obj(env); 644 | args.append_obj(appargs); 645 | args.append_obj(options); 646 | 647 | CFRelease(v1); 648 | CFRelease(v0); 649 | CFRelease(options); 650 | CFRelease(env); 651 | CFRelease(appargs); 652 | CFRelease(bid); 653 | CFRelease(path); 654 | 655 | CFTypeRef retobj = NULL; 656 | 657 | if ( !send_message(conn, channel, CFSTR("launchSuspendedProcessWithDevicePath:bundleIdentifier:environment:arguments:options:"), &args) 658 | || !recv_message(conn, &retobj, NULL) 659 | || retobj == NULL ) 660 | { 661 | qeprintf("Error: failed to launch %s\n", _bid); 662 | return false; 663 | } 664 | 665 | bool ok = true; 666 | if ( CFGetTypeID(retobj) == CFNumberGetTypeID() ) 667 | { 668 | CFNumberRef _pid = (CFNumberRef)retobj; 669 | int pid = 0; 670 | CFNumberGetValue(_pid, kCFNumberSInt32Type, &pid); 671 | qprintf("pid: %d\n", pid); 672 | } 673 | else 674 | { 675 | qeprintf("failed to retrieve the process ID: %s\n", get_description(retobj).c_str()); 676 | ok = false; 677 | } 678 | 679 | CFRelease(retobj); 680 | return ok; 681 | } 682 | 683 | //----------------------------------------------------------------------------- 684 | static void usage(const char *prog) 685 | { 686 | qeprintf("usage: %s [-v] [-d ] TASK \n" 687 | "\n" 688 | "This is a sample client application for the iOS Instruments server.\n" 689 | "It is capable of rudimentary communication with the server and can\n" 690 | "ask it to perform some interesting tasks.\n" 691 | "\n" 692 | "TASK can be one of the following:\n" 693 | " proclist - print a list of running processes\n" 694 | " applist - print a list of installed applications\n" 695 | " launch - launch a given app. provide the bundle id of the app to launch\n" 696 | " kill - kill a given process. provide the pid of the process to kill\n" 697 | "\n" 698 | "other args:\n" 699 | " -v more verbose output\n" 700 | " -d device ID. if empty, this app will use the first device it finds\n", prog); 701 | } 702 | 703 | //----------------------------------------------------------------------------- 704 | static bool parse_args(int argc, const char **argv) 705 | { 706 | if ( argc > 1 ) 707 | { 708 | for ( int i = 1; i < argc; ) 709 | { 710 | if ( streq("-v", argv[i]) ) 711 | { 712 | verbose = true; 713 | i++; 714 | continue; 715 | } 716 | else if ( streq("-d", argv[i]) ) 717 | { 718 | if ( i == argc - 1 ) 719 | { 720 | qeprintf("Error: -d option requires a device id string\n"); 721 | break; 722 | } 723 | device_id = argv[i+1]; 724 | i += 2; 725 | continue; 726 | } 727 | 728 | qstring task = argv[i]; 729 | 730 | if ( task == "proclist" ) 731 | { 732 | proclist = true; 733 | return true; 734 | } 735 | else if ( task == "applist" ) 736 | { 737 | applist = true; 738 | return true; 739 | } 740 | else if ( task == "kill" ) 741 | { 742 | if ( i == argc - 1 ) 743 | { 744 | qeprintf("Error: \"kill\" requires a process id\n"); 745 | break; 746 | } 747 | pid2kill = atoi(argv[i+1]); 748 | return true; 749 | } 750 | else if ( task == "launch" ) 751 | { 752 | if ( i == argc - 1 ) 753 | { 754 | qeprintf("Error: \"launch\" requires a bundle id\n"); 755 | break; 756 | } 757 | bid2launch = argv[i+1]; 758 | return true; 759 | } 760 | 761 | qeprintf("Error, invalid task: %s\n", task.c_str()); 762 | break; 763 | } 764 | } 765 | 766 | usage(argv[0]); 767 | return false; 768 | } 769 | 770 | //----------------------------------------------------------------------------- 771 | int main(int argc, const char **argv) 772 | { 773 | if ( !parse_args(argc, argv) ) 774 | return EXIT_FAILURE; 775 | 776 | if ( !load_mobile_device() ) 777 | return EXIT_FAILURE; 778 | 779 | am_device_service_connection *conn = start_server(); 780 | if ( conn == NULL ) 781 | return EXIT_FAILURE; 782 | 783 | bool ok = false; 784 | if ( perform_handshake(conn) ) 785 | { 786 | if ( proclist ) 787 | ok = print_proclist(conn); 788 | else if ( applist ) 789 | ok = print_applist(conn); 790 | else if ( pid2kill > 0 ) 791 | ok = kill(conn, pid2kill); 792 | else if ( bid2launch != NULL ) 793 | ok = launch(conn, bid2launch); 794 | else 795 | ok = true; 796 | 797 | CFRelease(channels); 798 | } 799 | 800 | AMDServiceConnectionInvalidate(conn); 801 | CFRelease(conn); 802 | 803 | return ok ? EXIT_SUCCESS : EXIT_FAILURE; 804 | } 805 | -------------------------------------------------------------------------------- /dtxmsg_client.h: -------------------------------------------------------------------------------- 1 | #ifndef DTXMSG_CLIENT_H 2 | #define DTXMSG_CLIENT_H 3 | 4 | #include "dtxmsg_common.h" 5 | #include 6 | #include 7 | 8 | // reverse-engineered types from MobileDevice.framework 9 | #define kAMDSuccess ERR_SUCCESS 10 | 11 | // opaque structures 12 | struct am_device; 13 | struct am_device_notification; 14 | struct am_device_service_connection; 15 | 16 | #define ADNCI_MSG_CONNECTED 1 17 | #define ADNCI_MSG_DISCONNECTED 2 18 | #define ADNCI_MSG_UNKNOWN 3 19 | 20 | // callback info for AMDeviceNotificationSubscribe() 21 | struct am_device_notification_callback_info 22 | { 23 | am_device *dev; // device handle 24 | uint32 code; // one of ADNCI_MSG_... 25 | }; 26 | typedef void am_device_notification_callback_t(am_device_notification_callback_info *cbi, void *arg); 27 | 28 | #endif // DTXMSG_CLIENT_H 29 | -------------------------------------------------------------------------------- /dtxmsg_common.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "dtxmsg_common.h" 3 | 4 | //------------------------------------------------------------------------------ 5 | qstring to_qstring(CFStringRef ref) 6 | { 7 | if ( ref == NULL ) 8 | return ""; 9 | 10 | CFIndex length = CFStringGetLength(ref); 11 | if ( length <= 0 ) 12 | return ""; 13 | 14 | CFIndex bufsize = CFStringGetMaximumSizeForEncoding(length, kCFStringEncodingUTF8) + 1; 15 | char *buf = (char *)qalloc(bufsize); 16 | 17 | qstring ret; 18 | if ( CFStringGetCString(ref, buf, bufsize, kCFStringEncodingUTF8) ) 19 | ret.inject(buf); 20 | 21 | return ret; 22 | } 23 | 24 | //------------------------------------------------------------------------------ 25 | qstring get_description(CFTypeRef ref) 26 | { 27 | CFStringRef desc = CFCopyDescription(ref); 28 | qstring ret = to_qstring(desc); 29 | CFRelease(desc); 30 | return ret; 31 | } 32 | 33 | //----------------------------------------------------------------------------- 34 | void archive(bytevec_t *buf, CFTypeRef ref) 35 | { 36 | @autoreleasepool 37 | { 38 | id object = (__bridge id)ref; 39 | NSData *data = [NSKeyedArchiver archivedDataWithRootObject:object]; 40 | const void *bytes = [data bytes]; 41 | int length = [data length]; 42 | buf->append(bytes, length); 43 | } 44 | } 45 | 46 | //----------------------------------------------------------------------------- 47 | CFTypeRef unarchive(const uchar *buf, size_t bufsize) 48 | { 49 | @autoreleasepool 50 | { 51 | NSData *data = [NSData dataWithBytesNoCopy:(void *)buf length:bufsize freeWhenDone:false]; 52 | id object = [NSKeyedUnarchiver unarchiveObjectWithData:data]; 53 | return (__bridge CFTypeRef)[object retain]; 54 | } 55 | } 56 | 57 | //----------------------------------------------------------------------------- 58 | CFArrayRef deserialize( 59 | const uchar *buf, 60 | size_t bufsize, 61 | qstring *errbuf) 62 | { 63 | if ( bufsize < 16 ) 64 | { 65 | errbuf->sprnt("Error: buffer of size 0x%lx is too small for a serialized array", bufsize); 66 | return NULL; 67 | } 68 | 69 | uint64 size = *((uint64 *)buf+1); 70 | if ( size > bufsize ) 71 | { 72 | errbuf->sprnt("size of array object (%llx) is larger than total length of data (%lx)", size, bufsize); 73 | return NULL; 74 | } 75 | 76 | CFMutableArrayRef array = CFArrayCreateMutable(NULL, 0, &kCFTypeArrayCallBacks); 77 | 78 | uint64 off = sizeof(uint64) * 2; 79 | uint64 end = off + size; 80 | 81 | while ( off < end ) 82 | { 83 | int length = 0; 84 | int type = *((int *)(buf+off)); 85 | off += sizeof(int); 86 | 87 | CFTypeRef ref = NULL; 88 | 89 | switch ( type ) 90 | { 91 | case 2: 92 | // archived object 93 | length = *((int *)(buf+off)); 94 | off += sizeof(int); 95 | ref = unarchive(buf+off, length); 96 | break; 97 | 98 | case 3: 99 | case 5: 100 | // 32-bit int 101 | ref = CFNumberCreate(NULL, kCFNumberSInt32Type, buf+off); 102 | length = 4; 103 | break; 104 | 105 | case 4: 106 | case 6: 107 | // 64-bit int 108 | ref = CFNumberCreate(NULL, kCFNumberSInt64Type, buf+off); 109 | length = 8; 110 | break; 111 | 112 | case 10: 113 | // dictionary key. for arrays, the keys are empty and we ignore them 114 | continue; 115 | 116 | default: 117 | // there are more. we will deal with them as necessary 118 | break; 119 | } 120 | 121 | if ( ref == NULL ) 122 | { 123 | errbuf->sprnt("invalid object at offset %llx, type: %d\n", off, type); 124 | return NULL; 125 | } 126 | 127 | CFArrayAppendValue(array, ref); 128 | CFRelease(ref); 129 | off += length; 130 | } 131 | 132 | return (CFArrayRef)array; 133 | } 134 | 135 | //----------------------------------------------------------------------------- 136 | void append_d(bytevec_t &out, uint32 num) 137 | { 138 | out.append(&num, sizeof(num)); 139 | } 140 | 141 | //----------------------------------------------------------------------------- 142 | void append_q(bytevec_t &out, uint64 num) 143 | { 144 | out.append(&num, sizeof(num)); 145 | } 146 | 147 | //----------------------------------------------------------------------------- 148 | void append_b(bytevec_t &out, const bytevec_t &bv) 149 | { 150 | out.append(bv.begin(), bv.size()); 151 | } 152 | 153 | //----------------------------------------------------------------------------- 154 | void append_v(bytevec_t &out, const void *v, size_t len) 155 | { 156 | out.append(v, len); 157 | } 158 | 159 | //----------------------------------------------------------------------------- 160 | void message_aux_t::append_int(int32 val) 161 | { 162 | append_d(buf, 10); // empty dictionary key 163 | append_d(buf, 3); // 32-bit int 164 | append_d(buf, val); 165 | } 166 | 167 | //----------------------------------------------------------------------------- 168 | void message_aux_t::append_long(int64 val) 169 | { 170 | append_d(buf, 10); // empty dictionary key 171 | append_d(buf, 4); // 64-bit int 172 | append_q(buf, val); 173 | } 174 | 175 | //----------------------------------------------------------------------------- 176 | void message_aux_t::append_obj(CFTypeRef obj) 177 | { 178 | append_d(buf, 10); // empty dictionary key 179 | append_d(buf, 2); // archived object 180 | 181 | bytevec_t tmp; 182 | archive(&tmp, obj); 183 | 184 | append_d(buf, tmp.size()); 185 | append_b(buf, tmp); 186 | } 187 | 188 | //----------------------------------------------------------------------------- 189 | void message_aux_t::get_bytes(bytevec_t *out) const 190 | { 191 | if ( !buf.empty() ) 192 | { 193 | // the final serialized array must start with a magic qword, 194 | // followed by the total length of the array data as a qword, 195 | // followed by the array data itself. 196 | append_q(*out, 0x1F0); 197 | append_q(*out, buf.size()); 198 | append_b(*out, buf); 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /dtxmsg_common.h: -------------------------------------------------------------------------------- 1 | #ifndef DTXMSG_COMMON_H 2 | #define DTXMSG_COMMON_H 3 | 4 | #include 5 | #include 6 | 7 | //------------------------------------------------------------------------------ 8 | qstring to_qstring(CFStringRef ref); 9 | 10 | //------------------------------------------------------------------------------ 11 | qstring get_description(CFTypeRef ref); 12 | 13 | //------------------------------------------------------------------------------ 14 | void archive(bytevec_t *buf, CFTypeRef ref); 15 | 16 | //------------------------------------------------------------------------------ 17 | CFTypeRef unarchive(const uchar *buf, size_t bufsize); 18 | 19 | //------------------------------------------------------------------------------ 20 | // convert a serialized array to a CFArrayRef object 21 | CFArrayRef deserialize(const uchar *buf, size_t bufsize, qstring *errbuf = NULL); 22 | 23 | //------------------------------------------------------------------------------ 24 | // helper class for serializing method arguments 25 | class message_aux_t 26 | { 27 | bytevec_t buf; 28 | 29 | public: 30 | void append_int(int32 val); 31 | void append_long(int64 val); 32 | void append_obj(CFTypeRef obj); 33 | 34 | void get_bytes(bytevec_t *out) const; 35 | }; 36 | 37 | //----------------------------------------------------------------------------- 38 | void append_d(bytevec_t &out, uint32 num); 39 | void append_q(bytevec_t &out, uint64 num); 40 | void append_b(bytevec_t &out, const bytevec_t &bv); 41 | void append_v(bytevec_t &out, const void *v, size_t len); 42 | 43 | //----------------------------------------------------------------------------- 44 | struct DTXMessageHeader 45 | { 46 | uint32 magic; 47 | uint32 cb; 48 | uint16 fragmentId; 49 | uint16 fragmentCount; 50 | uint32 length; 51 | uint32 identifier; 52 | uint32 conversationIndex; 53 | uint32 channelCode; 54 | uint32 expectsReply; 55 | }; 56 | 57 | //----------------------------------------------------------------------------- 58 | struct DTXMessagePayloadHeader 59 | { 60 | uint32 flags; 61 | uint32 auxiliaryLength; 62 | uint64 totalLength; 63 | }; 64 | 65 | #endif // DTXMSG_COMMON_H 66 | -------------------------------------------------------------------------------- /makefile: -------------------------------------------------------------------------------- 1 | PROC = dtxmsg 2 | O1 = dtxmsg_common 3 | 4 | # dtxmsg_client must link against pro.a/dumb.o, which are only available for __EA64__ builds 5 | ifdef __EA64__ 6 | # the dtxmsg_client app is now deprecated. please use this utility intead: https://github.com/troybowman/ios_instruments_client 7 | #GOALS += dtxmsg_client 8 | endif 9 | 10 | include ../plugin.mak 11 | 12 | STDLIBS += -lobjc -framework Foundation -framework CoreFoundation 13 | 14 | ifneq ($(IDA_INSTALL_DIR),) 15 | POSTACTION = cp $@ $(IDA_INSTALL_DIR)/ida.app/Contents/MacOS/plugins 16 | endif 17 | 18 | $(F)dtxmsg_common$(O): CFLAGS += -x objective-c++ 19 | 20 | .PHONY: dtxmsg_client 21 | dtxmsg_client: $(R)dtxmsg_client 22 | $(R)dtxmsg_client: $(call dumb_target, pro, $(F)dtxmsg_common$(O) $(F)dtxmsg_client$(O)) 23 | 24 | # MAKEDEP dependency list ------------------ 25 | $(F)dtxmsg$(O) : $(I)bitrange.hpp $(I)bytes.hpp $(I)config.hpp \ 26 | $(I)dbg.hpp $(I)err.h $(I)expr.hpp $(I)fpro.h \ 27 | $(I)funcs.hpp $(I)gdl.hpp $(I)hexrays.hpp $(I)ida.hpp \ 28 | $(I)idd.hpp $(I)idp.hpp $(I)ieee.h $(I)kernwin.hpp \ 29 | $(I)lines.hpp $(I)llong.hpp $(I)loader.hpp $(I)moves.hpp \ 30 | $(I)nalt.hpp $(I)name.hpp $(I)netnode.hpp $(I)pro.h \ 31 | $(I)range.hpp $(I)segment.hpp $(I)struct.hpp \ 32 | $(I)typeinf.hpp $(I)ua.hpp $(I)xref.hpp dtxmsg.cpp \ 33 | dtxmsg.h dtxmsg_common.h 34 | $(F)dtxmsg_common$(O): $(I)llong.hpp $(I)pro.h dtxmsg_common.cpp \ 35 | dtxmsg_common.h 36 | $(F)dtxmsg_client$(O): $(I)err.h $(I)fpro.h $(I)llong.hpp $(I)pro.h \ 37 | dtxmsg_client.cpp dtxmsg_client.h dtxmsg_common.h 38 | -------------------------------------------------------------------------------- /slides.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/troybowman/dtxmsg/d3a809305a82704af429430d6d5d9716a0519b29/slides.pdf --------------------------------------------------------------------------------