├── .gitignore ├── LICENSE ├── README.md ├── devel ├── README.md ├── __handlers__ │ ├── MMCGIWrap │ │ ├── setM_requestPb_.js │ │ └── setM_responsePb_.js │ ├── ProtobufCGIWrap │ │ ├── setM_pbRequest_.js │ │ └── setM_pbResponse_.js │ └── WeChat │ │ ├── __xlogger_Level_impl.js │ │ └── __xlogger_Write_impl.js ├── init.js ├── protobuf_config.py └── xlogger.d ├── macos ├── dbcracker.d └── eavesdropper.d └── pcbakchat ├── gather.d ├── proto └── BakChatMsgList.proto └── usage.md /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/** 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 QINGYAO SUN 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 | # WeChat Deciphers for macOS 2 | 3 | This project is grouped into three directories 4 | 5 | + The directory `macos/` holds DTrace scripts for messing with WeChat.app on macOS. 6 | + `eavesdropper.d` prints the conversation in real-time. It effectively shows database transactions on the fly. 7 | + `dbcracker.d` reveals locations of the encrypted SQLite3 databases and their credentials. *Since it can only capture secrets when WeChat.app opens these files, you need to perform a login while the script is running.* Simply copy & paste the script output to invoke [SQLCipher](https://github.com/sqlcipher/sqlcipher) and supply the respective `PRAGMA`s. 8 | + In `pcbakchat/` you can find scripts to parse WeChat's backup files. 9 | + `gather.d` gathers several pieces of intel required to decrypt the backup. 10 | + In `devel/` resides utilities for further reverse engineering. They are intended for hackers only, and the end-users of this project are not expected to use them. 11 | + `xlogger.d` prints the log messages going to `/Users/$USER/Library/Containers/com.tencent.xinWeChat/Data/Library/Caches/com.tencent.xinWeChat/2.0b4.0.9/log/*.xlog`. I made this script [destructive](http://dtrace.org/guide/chp-actsub.html#chp-actsub-4) to overwrite the global variable [`gs_level`](https://github.com/Tencent/mars/blob/master/mars/comm/xlogger/xloggerbase.c#L93). 12 | + `protobuf_config.py` describes the protobuf format used by the backup files for [protobuf-inspector](https://github.com/mildsunrise/protobuf-inspector). 13 | + `__handlers__/` contains some handlers to be used with `frida-trace`. 14 | + `init.js` contains the helper function for `frida-trace`. 15 | 16 | ## Dependencies 17 | 18 | Since `dtrace(1)` is pre-installed on macOS, no dependencies are required to run the scripts. However, you may need to [disable SIP](https://apple.stackexchange.com/questions/208762/now-that-el-capitan-is-rootless-is-there-any-way-to-get-dtrace-working) if you haven't done that yet. In addition, you'll need [SQLCipher](https://github.com/sqlcipher/sqlcipher) to inspect the databases discovered by `dbcracker.d`. 19 | 20 | For some scripts in `devel`, you will also need [Frida](https://frida.re) and a (preferably jailbroken) iOS device. 21 | 22 | ## Usage 23 | 24 | For DTrace scripts, launch WeChat and run 25 | 26 | ```bash 27 | sudo $DECIPHER_SCRIPT -p $(pgrep -f '^/Applications/WeChat.app/Contents/MacOS/WeChat') 28 | ``` 29 | 30 | replace `$DECIPHER_SCRIPT` with `macos/dbcracker.d`, `macos/eavesdropper.d`, `pcbakchat/gather.d`, or `devel/xlogger.d`. 31 | 32 | The stuff in `pcbakchat/` is a little involved. See `usage.md` for more details. 33 | 34 | ## Will Tencent ban my WeChat account? 35 | 36 | Hopefully not. Most processing is done offline on the macOS client, and the overhead of DTrace should be negligible, so there is little chance they will catch you. 37 | 38 | ## Version Information 39 | 40 | The production of these scripts involved an excess amount of guesswork and wishful thinking, but at least it works on my machine :) 41 | 42 | ``` 43 | Device Type: MacBookPro14,1 44 | System Version: Version 10.14.6 (Build 18G8022) 45 | System Language: en 46 | WeChat Version: [2021-04-02 17:49:14] v3.0.1.16 (17837) #36bbf5f7d2 47 | WeChat Language: en 48 | Historic Version: [2021-03-29 20:23:50] v3.0.0.16 (17816) #2a4801bee9 49 | Network Status: Reachable via WiFi or Ethernet 50 | Display: *(1440x900)/Retina 51 | ``` 52 | -------------------------------------------------------------------------------- /devel/README.md: -------------------------------------------------------------------------------- 1 | # Note to Hackers 2 | 3 | WeChat makes extensive use of [Protocol Buffers](https://developers.google.com/protocol-buffers), but the code generated by `protoc` has been heavily modified to fit into mobile devices, hindering our reverse engineering. Fortunately, the article [iOS微信安装包瘦身](https://cloud.tencent.com/developer/article/1030792) provides a decent overview of protobuf's integration into WeChat's iOS client. Based on the information, we created a helper function `show_fields_recursive` to dissect instances of subclasses of `WXPBGeneratedMessage`. The function resides in `init.js` and should work for both iOS and macOS. 4 | 5 | + For iOS, run `frida-trace -U -n WeChat -m "-[ProtobufCGIWrap setM_pbRequest:]" -m "-[ProtobufCGIWrap setM_pbResponse:]" --init-session=init.js` to dump the client's communication with the server. You need to [install Frida on your iOS device](https://frida.re/docs/ios/) first. 6 | + For macOS, use `frida-trace -n WeChat -m "-[MMCGIWrap setM_requestPb:]" -m "-[MMCGIWrap setM_responsePb:]" --init-session init.js`. Without the `-U` switch, Frida attaches to the WeChat process on macOS instead. 7 | 8 | In addition, apart from `xlogger.d`, you can also use Frida to inspect the log messages. Simply run `frida-trace -U -n WeChat -i "__xlogger_Write_impl" -i "__xlogger_Level_impl"` for iOS, or `frida-trace -n WeChat -i "__xlogger_Write_impl" -i "__xlogger_Level_impl"` for macOS. 9 | 10 | ## Overview of the Backup Process 11 | 12 | In this part, we briefly describe the process of a backup from WeChat's iOS client to its macOS client. Only key steps are covered. 13 | 14 | 1. Both clients get a 256-bit "encryptkey" from the remote server. 15 | + This is *NOT* the same key as used by `macos/dbcracker.d`. 16 | + The ASCII representation of the key consists of 32 hexadecimal digits, so there are merely 128 bits of entropy. However, we consider it a 256-bit key anyway since it still takes 256 bits of space to store. 17 | 2. On the iOS device, the relevant chat history is fetched from the [wcdb](https://github.com/Tencent/wcdb) subsystem into `CMessageWrap` objects in `-[WXGBackupMMDB getMsgUseBatchQuery:fromRowID:fromCreateTime:endAtTime:timeAsend:]`. 18 | 3. The `CMessageWrap` objects are somehow transformed into several `BakChatMsgList` objects. There appears to be one `BakChatMsgList` per contact. 19 | 4. The `BakChatMsgList` objects are serialized into protobuf (with `-[WXPBGeneratedMessage serializedData]`), encrypted in AES-128-ECB (with `-[CAESCrypt initECBEncryptWithKey:]` and `-[CAESCrypt encryptECBWithData:Final:]`). 20 | + The encrypt key is the first 128 bits (i.e. first half) of "encryptkey". 21 | + `BakChatMsgList` is a subclass of `WXPBGeneratedMessage`, so its instances can be understood by `show_fields_recursive`, based on whose output we reconstructed `pcbakchat/proto/BakChatMsgList.proto`. 22 | 5. Media data are also encrypted in AES-128-ECB (in `-[WXGBackupDataMgr purgeMediaArray:]`). 23 | 6. The encrypted protobufs are then packed with metadata like `dataID` (presumably an identifier of the chat history) into a "datapush", which is another layer of protobuf (in `-[WXGBackupDataMgr getBackupDataPushFromBakChatMsgList:withDataID:]`). 24 | 7. The datapushes are encrypted in RC4 and sent to the macOS client over Wi-Fi. 25 | + The encrypt key is the 256-bit "encryptkey" in all its entirety. 26 | 8. On the macOS client, datapushes are decrypted in RC4 (with `CCCryptor`) and deserialized. 27 | + The macOS client appears to re-encrypt the datapushes after decryption, presumably with some modification to the payload. TODO: investigate this behavior. 28 | 9. Finally, `dataID` is extracted and used to dispatch the encrypted "inner" protobuf to one of `BAK_X_TEXT`. 29 | 30 | **TL;DR:** the chat history is packed, encrypted in AES-128-ECB, packed with some metadata, encrypted with RC4, sent over Wi-Fi, decrypted (RC4), unpacked, and stored to the disk. -------------------------------------------------------------------------------- /devel/__handlers__/MMCGIWrap/setM_requestPb_.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Auto-generated by Frida. Please modify to match the signature of -[MMCGIWrap setM_requestPb:]. 3 | * This stub is currently auto-generated from manpages when available. 4 | * 5 | * For full API reference, see: https://frida.re/docs/javascript-api/ 6 | */ 7 | 8 | { 9 | /** 10 | * Called synchronously when about to call -[MMCGIWrap setM_requestPb:]. 11 | * 12 | * @this {object} - Object allowing you to store state for use in onLeave. 13 | * @param {function} log - Call this function with a string to be presented to the user. 14 | * @param {array} args - Function arguments represented as an array of NativePointer objects. 15 | * For example use args[0].readUtf8String() if the first argument is a pointer to a C string encoded as UTF-8. 16 | * It is also possible to modify arguments by assigning a NativePointer object to an element of this array. 17 | * @param {object} state - Object allowing you to keep state across function calls. 18 | * Only one JavaScript function will execute at a time, so do not worry about race-conditions. 19 | * However, do not use this to store function arguments across onEnter/onLeave, but instead 20 | * use "this" which is an object for keeping state local to an invocation. 21 | */ 22 | onEnter(log, args, state) { 23 | log(`-[MMCGIWrap setM_requestPb:${args[2]}]`); 24 | const m_requestPb = new ObjC.Object(args[2]); 25 | show_fields_recursive(m_requestPb, 'm_requestPb', 0, log); 26 | }, 27 | 28 | /** 29 | * Called synchronously when about to return from -[MMCGIWrap setM_requestPb:]. 30 | * 31 | * See onEnter for details. 32 | * 33 | * @this {object} - Object allowing you to access state stored in onEnter. 34 | * @param {function} log - Call this function with a string to be presented to the user. 35 | * @param {NativePointer} retval - Return value represented as a NativePointer object. 36 | * @param {object} state - Object allowing you to keep state across function calls. 37 | */ 38 | onLeave(log, retval, state) { 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /devel/__handlers__/MMCGIWrap/setM_responsePb_.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Auto-generated by Frida. Please modify to match the signature of -[MMCGIWrap setM_responsePb:]. 3 | * This stub is currently auto-generated from manpages when available. 4 | * 5 | * For full API reference, see: https://frida.re/docs/javascript-api/ 6 | */ 7 | 8 | { 9 | /** 10 | * Called synchronously when about to call -[MMCGIWrap setM_responsePb:]. 11 | * 12 | * @this {object} - Object allowing you to store state for use in onLeave. 13 | * @param {function} log - Call this function with a string to be presented to the user. 14 | * @param {array} args - Function arguments represented as an array of NativePointer objects. 15 | * For example use args[0].readUtf8String() if the first argument is a pointer to a C string encoded as UTF-8. 16 | * It is also possible to modify arguments by assigning a NativePointer object to an element of this array. 17 | * @param {object} state - Object allowing you to keep state across function calls. 18 | * Only one JavaScript function will execute at a time, so do not worry about race-conditions. 19 | * However, do not use this to store function arguments across onEnter/onLeave, but instead 20 | * use "this" which is an object for keeping state local to an invocation. 21 | */ 22 | onEnter(log, args, state) { 23 | log(`-[MMCGIWrap setM_responsePb:${args[2]}]`); 24 | const m_responsePb = new ObjC.Object(args[2]); 25 | show_fields_recursive(m_responsePb, 'm_responsePb', 0, log); 26 | }, 27 | 28 | /** 29 | * Called synchronously when about to return from -[MMCGIWrap setM_responsePb:]. 30 | * 31 | * See onEnter for details. 32 | * 33 | * @this {object} - Object allowing you to access state stored in onEnter. 34 | * @param {function} log - Call this function with a string to be presented to the user. 35 | * @param {NativePointer} retval - Return value represented as a NativePointer object. 36 | * @param {object} state - Object allowing you to keep state across function calls. 37 | */ 38 | onLeave(log, retval, state) { 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /devel/__handlers__/ProtobufCGIWrap/setM_pbRequest_.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Auto-generated by Frida. Please modify to match the signature of -[ProtobufCGIWrap setM_pbRequest:]. 3 | * This stub is currently auto-generated from manpages when available. 4 | * 5 | * For full API reference, see: https://frida.re/docs/javascript-api/ 6 | */ 7 | 8 | { 9 | /** 10 | * Called synchronously when about to call -[ProtobufCGIWrap setM_pbRequest:]. 11 | * 12 | * @this {object} - Object allowing you to store state for use in onLeave. 13 | * @param {function} log - Call this function with a string to be presented to the user. 14 | * @param {array} args - Function arguments represented as an array of NativePointer objects. 15 | * For example use args[0].readUtf8String() if the first argument is a pointer to a C string encoded as UTF-8. 16 | * It is also possible to modify arguments by assigning a NativePointer object to an element of this array. 17 | * @param {object} state - Object allowing you to keep state across function calls. 18 | * Only one JavaScript function will execute at a time, so do not worry about race-conditions. 19 | * However, do not use this to store function arguments across onEnter/onLeave, but instead 20 | * use "this" which is an object for keeping state local to an invocation. 21 | */ 22 | onEnter(log, args, state) { 23 | log(`-[ProtobufCGIWrap setM_pbRequest:${args[2]}]`); 24 | const pbRequest = new ObjC.Object(args[2]); 25 | show_fields_recursive(pbRequest, 'pbRequest', 0, log); 26 | }, 27 | 28 | /** 29 | * Called synchronously when about to return from -[ProtobufCGIWrap setM_pbRequest:]. 30 | * 31 | * See onEnter for details. 32 | * 33 | * @this {object} - Object allowing you to access state stored in onEnter. 34 | * @param {function} log - Call this function with a string to be presented to the user. 35 | * @param {NativePointer} retval - Return value represented as a NativePointer object. 36 | * @param {object} state - Object allowing you to keep state across function calls. 37 | */ 38 | onLeave(log, retval, state) { 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /devel/__handlers__/ProtobufCGIWrap/setM_pbResponse_.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Auto-generated by Frida. Please modify to match the signature of -[ProtobufCGIWrap setM_pbResponse:]. 3 | * This stub is currently auto-generated from manpages when available. 4 | * 5 | * For full API reference, see: https://frida.re/docs/javascript-api/ 6 | */ 7 | 8 | { 9 | /** 10 | * Called synchronously when about to call -[ProtobufCGIWrap setM_pbResponse:]. 11 | * 12 | * @this {object} - Object allowing you to store state for use in onLeave. 13 | * @param {function} log - Call this function with a string to be presented to the user. 14 | * @param {array} args - Function arguments represented as an array of NativePointer objects. 15 | * For example use args[0].readUtf8String() if the first argument is a pointer to a C string encoded as UTF-8. 16 | * It is also possible to modify arguments by assigning a NativePointer object to an element of this array. 17 | * @param {object} state - Object allowing you to keep state across function calls. 18 | * Only one JavaScript function will execute at a time, so do not worry about race-conditions. 19 | * However, do not use this to store function arguments across onEnter/onLeave, but instead 20 | * use "this" which is an object for keeping state local to an invocation. 21 | */ 22 | onEnter(log, args, state) { 23 | log(`-[ProtobufCGIWrap setM_pbResponse:${args[2]}]`); 24 | const pbResponse = new ObjC.Object(args[2]); 25 | show_fields_recursive(pbResponse, 'pbResponse', 0, log); 26 | }, 27 | 28 | /** 29 | * Called synchronously when about to return from -[ProtobufCGIWrap setM_pbResponse:]. 30 | * 31 | * See onEnter for details. 32 | * 33 | * @this {object} - Object allowing you to access state stored in onEnter. 34 | * @param {function} log - Call this function with a string to be presented to the user. 35 | * @param {NativePointer} retval - Return value represented as a NativePointer object. 36 | * @param {object} state - Object allowing you to keep state across function calls. 37 | */ 38 | onLeave(log, retval, state) { 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /devel/__handlers__/WeChat/__xlogger_Level_impl.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Auto-generated by Frida. Please modify to match the signature of __xlogger_Level_impl. 3 | * This stub is currently auto-generated from manpages when available. 4 | * 5 | * For full API reference, see: https://frida.re/docs/javascript-api/ 6 | */ 7 | 8 | { 9 | /** 10 | * Called synchronously when about to call __xlogger_Level_impl. 11 | * 12 | * @this {object} - Object allowing you to store state for use in onLeave. 13 | * @param {function} log - Call this function with a string to be presented to the user. 14 | * @param {array} args - Function arguments represented as an array of NativePointer objects. 15 | * For example use args[0].readUtf8String() if the first argument is a pointer to a C string encoded as UTF-8. 16 | * It is also possible to modify arguments by assigning a NativePointer object to an element of this array. 17 | * @param {object} state - Object allowing you to keep state across function calls. 18 | * Only one JavaScript function will execute at a time, so do not worry about race-conditions. 19 | * However, do not use this to store function arguments across onEnter/onLeave, but instead 20 | * use "this" which is an object for keeping state local to an invocation. 21 | */ 22 | onEnter(log, args, state) { 23 | }, 24 | 25 | /** 26 | * Called synchronously when about to return from __xlogger_Level_impl. 27 | * 28 | * See onEnter for details. 29 | * 30 | * @this {object} - Object allowing you to access state stored in onEnter. 31 | * @param {function} log - Call this function with a string to be presented to the user. 32 | * @param {NativePointer} retval - Return value represented as a NativePointer object. 33 | * @param {object} state - Object allowing you to keep state across function calls. 34 | */ 35 | onLeave(log, retval, state) { 36 | retval.replace(0); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /devel/__handlers__/WeChat/__xlogger_Write_impl.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Auto-generated by Frida. Please modify to match the signature of __xlogger_Write_impl. 3 | * This stub is currently auto-generated from manpages when available. 4 | * 5 | * For full API reference, see: https://frida.re/docs/javascript-api/ 6 | */ 7 | 8 | { 9 | /** 10 | * Called synchronously when about to call __xlogger_Write_impl. 11 | * 12 | * @this {object} - Object allowing you to store state for use in onLeave. 13 | * @param {function} log - Call this function with a string to be presented to the user. 14 | * @param {array} args - Function arguments represented as an array of NativePointer objects. 15 | * For example use args[0].readUtf8String() if the first argument is a pointer to a C string encoded as UTF-8. 16 | * It is also possible to modify arguments by assigning a NativePointer object to an element of this array. 17 | * @param {object} state - Object allowing you to keep state across function calls. 18 | * Only one JavaScript function will execute at a time, so do not worry about race-conditions. 19 | * However, do not use this to store function arguments across onEnter/onLeave, but instead 20 | * use "this" which is an object for keeping state local to an invocation. 21 | */ 22 | onEnter(log, args, state) { 23 | var message = args[1].readUtf8String(); 24 | if (!message.match(/DEBUG: check memory footprint \d+ MB/) 25 | && !message.match(/INFO: check memory footprint \d+ MB/) 26 | && !message.match(/get real origin host : /) 27 | && !message.match(/INFO: onServiceMemoryWarning: /) 28 | && !message.match(/Mission Completed!/) 29 | && !message.match(/VERBOSE: /) 30 | && !message.match(/DEBUG: /)) { 31 | log(message 32 | + '\nBacktrace: \n' 33 | + Thread.backtrace(this.context, Backtracer.ACCURATE).map(DebugSymbol.fromAddress).join('\n')); 34 | } 35 | }, 36 | 37 | /** 38 | * Called synchronously when about to return from __xlogger_Write_impl. 39 | * 40 | * See onEnter for details. 41 | * 42 | * @this {object} - Object allowing you to access state stored in onEnter. 43 | * @param {function} log - Call this function with a string to be presented to the user. 44 | * @param {NativePointer} retval - Return value represented as a NativePointer object. 45 | * @param {object} state - Object allowing you to keep state across function calls. 46 | */ 47 | onLeave(log, retval, state) { 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /devel/init.js: -------------------------------------------------------------------------------- 1 | const labels = [ 2 | '!!! LABEL_ERROR !!!', 3 | 'optional', 4 | 'required', 5 | 'repeated' 6 | ] 7 | const types = [ 8 | '!!! TYPE_ERROR !!!', 9 | 'double', 10 | 'float', 11 | 'int64', 12 | 'uint64', 13 | 'int32', 14 | 'fixed64', 15 | 'fixed32', 16 | 'bool', 17 | 'string', 18 | 'TYPE_GROUP', 19 | 'TYPE_MESSAGE', 20 | 'bytes', 21 | 'uint32', 22 | 'TYPE_ENUM', 23 | 'sfixed32', 24 | 'sfixed64', 25 | 'sint32', 26 | 'sint64', 27 | ] 28 | 29 | 30 | /* 31 | * Based on the blog post "iOS微信安装包瘦身" by We Mobile Dev 32 | * https://cloud.tencent.com/developer/article/1030792 33 | */ 34 | function show_fields_recursive(inst, name, indent, log) { 35 | const header = `[ENTRY] ${name} (${inst.$className})`; 36 | log(); 37 | log(`${' '.repeat(indent * 4)}${header}`); 38 | log(`${' '.repeat(indent * 4)}${'='.repeat(header.length)}`); 39 | 40 | var items = []; 41 | 42 | Object.keys(inst.$ivars).forEach(function(k) { 43 | const v = inst.$ivars[k]; 44 | log(`${' '.repeat(indent * 4)}$ivars['${k}'] = ${v}`); 45 | if (v && 46 | k !== 'isa' && 47 | v.$superClass !== undefined && 48 | v.$superClass.$className === 'PBGeneratedMessage') { 49 | items.push([v, `${name}.${k}`]); 50 | } 51 | }); 52 | 53 | const classInfo = inst.$ivars['_classInfo']; // PBClassInfo 54 | 55 | if (!classInfo.isNull()) { 56 | const numberOfProperty = classInfo.readUInt(); 57 | 58 | log(`${' '.repeat(indent * 4)}${'-'.repeat(header.length)}`); 59 | const propertyNamesBase = classInfo.add(8).readPointer(); 60 | const fieldInfosBase = classInfo.add(32).readPointer(); 61 | for (let i = 0; i < numberOfProperty; i++) { 62 | const propertyName = propertyNamesBase 63 | .add(8 * i) 64 | .readPointer() 65 | .readUtf8String(); 66 | const fieldType = fieldInfosBase 67 | .add(24 * i + 2) 68 | .readU8(); 69 | const val = inst[propertyName](); 70 | log(`${' '.repeat(indent * 4)}${propertyName} = ${val}`); 71 | if (fieldType === 11) { 72 | if (val) { 73 | const property = new ObjC.Object(val); 74 | if (property.$className === '__NSArrayM') { 75 | for (let i = 0; i < property.count(); i++) { 76 | items.push([property.objectAtIndex_(i), `${name}.${propertyName}[${i}]`]); 77 | } 78 | } else { 79 | items.push([property, `${name}.${propertyName}`]); 80 | } 81 | } 82 | } 83 | } 84 | 85 | log(`${' '.repeat(indent * 4)}${'-'.repeat(header.length)}`); 86 | for (let i = 0; i < numberOfProperty; i++) { 87 | const propertyName = propertyNamesBase 88 | .add(8 * i) 89 | .readPointer() 90 | .readUtf8String(); 91 | const fieldNumber = fieldInfosBase 92 | .add(24 * i) 93 | .readU8(); 94 | const fieldLabel = fieldInfosBase 95 | .add(24 * i + 1) 96 | .readU8(); 97 | const fieldType = fieldInfosBase 98 | .add(24 * i + 2) 99 | .readU8(); 100 | const isPacked = fieldInfosBase 101 | .add(24 * i + 3) 102 | .readU8(); 103 | log(`${' '.repeat(indent * 4)}${labels[fieldLabel]} ${types[fieldType]} ${propertyName} = ${fieldNumber}${isPacked ? ' [packed=true]' : ''};`); 104 | } 105 | } 106 | 107 | items.forEach(function(item, index, array) { 108 | show_fields_recursive(item[0], item[1], indent+1, log); 109 | }) 110 | } 111 | 112 | -------------------------------------------------------------------------------- /devel/protobuf_config.py: -------------------------------------------------------------------------------- 1 | types = { 2 | "root": { 3 | 1: ("uint32", "count"), 4 | 2: ("BakChatMsgItem", "list"), 5 | }, 6 | "BakChatMsgItem": { 7 | 1: ("uint32", "type"), 8 | 2: ("string", "clientMsgId"), 9 | 3: ("SKBuiltinString_t", "fromUserName"), 10 | 4: ("SKBuiltinString_t", "toUserName"), 11 | 5: ("SKBuiltinString_t", "content"), 12 | 6: ("uint32", "msgStatus"), 13 | 7: ("uint32", "clientMsgTime"), 14 | 8: ("string", "msgSource"), 15 | 9: ("uint32", "msgId"), 16 | 10: ("uint32", "mediaIdCount"), 17 | 11: ("SKBuiltinString_t", "mediaId"), 18 | 12: ("SKBuiltinUint32_t", "mediaType"), 19 | 13: ("SKBuiltinBuffer_t", "buffer"), 20 | 14: ("uint32", "bufferLength"), 21 | 15: ("uint32", "bufferType"), 22 | 16: ("uint64", "newMsgId"), 23 | 17: ("uint32", "sequentId"), 24 | 18: ("int64", "clientMsgMillTime"), 25 | 19: ("uint32", "msgFlag"), 26 | }, 27 | "SKBuiltinString_t": { 28 | 1: ("string", "string"), 29 | }, 30 | "SKBuiltinUint32_t": { 31 | 1: ("uint32", "uiVal"), 32 | }, 33 | "SKBuiltinBuffer_t": { 34 | 1: ("uint32", "iLen"), 35 | 2: ("bytes", "buffer"), 36 | }, 37 | } 38 | -------------------------------------------------------------------------------- /devel/xlogger.d: -------------------------------------------------------------------------------- 1 | #!/usr/sbin/dtrace -w -s 2 | 3 | #pragma D option quiet 4 | 5 | /* 6 | * Set the global variable `gs_level' in xloggerbase.c to zero, so that 7 | * `xlogger_Level()' always return zero, enabling DEBUG level messages. 8 | * 9 | * TODO: 10 | * 1. Figure out why `gs_level' is automatically restored on exit. 11 | * 2. Decode the MOV instruction under PC instead of hardcoding the offset. 12 | */ 13 | pid$target:WeChat:__xlogger_Level_impl:entry 14 | { 15 | /* 16 | * The magic number is calcuated from Hopper's disassembly output. 17 | * No need to worry about ASLR since we only need relative offsets. 18 | * 19 | * For WeChat for macOS v3.0.1.16 (17837) #36bbf5f7d2, we have 20 | * 21 | * 0x152904a0 gs_level 22 | * - 0x13624e20 __xlogger_Level_impl 23 | * ------------ 24 | * 0x1c6b680 25 | */ 26 | gs_level_addr = uregs[R_PC] + 0x1c6b680; 27 | zero = (int *) alloca(4); 28 | *zero = 0; 29 | copyout(zero, gs_level_addr, 4); 30 | } 31 | 32 | pid$target:WeChat:__xlogger_Write_impl:entry 33 | { 34 | self->logbody = arg1; 35 | } 36 | 37 | /* 38 | * Log some extra information, e.g. the name of the source file/function. 39 | * See https://github.com/Tencent/mars/blob/master/mars/log/src/formater.cc 40 | */ 41 | pid$target:libsystem_c.dylib:snprintf:entry 42 | /self->logbody && arg1 == 1024/ 43 | { 44 | self->loghead = arg0; 45 | } 46 | 47 | pid$target:libsystem_c.dylib:snprintf:return 48 | /self->logbody && self->loghead/ 49 | { 50 | printf("[%Y] %s\n", walltimestamp, copyinstr(self->logbody)); 51 | 52 | /* In case you need extra verbosity 53 | * 54 | printf("[%Y] <%s> %s\n", 55 | walltimestamp, 56 | copyinstr(self->loghead), 57 | copyinstr(self->logbody)); 58 | */ 59 | 60 | self->logbody = self->loghead = 0; 61 | } 62 | -------------------------------------------------------------------------------- /macos/dbcracker.d: -------------------------------------------------------------------------------- 1 | #!/usr/sbin/dtrace -s 2 | 3 | #pragma D option quiet 4 | 5 | /* 6 | * TODO: Limit probing to a single CPU core to declutter the output. 7 | * (but what if the decryption isn't scheduled to that core?) 8 | */ 9 | 10 | /* 11 | * Adapted from a legacy version of SQLCipher (v3.15.2). 12 | * 13 | * https://github.com/Tencent/sqlcipher/blob/4f37c817eb99e18e4fdc8ac63d67ac33610d66be/src/crypto_impl.c 14 | */ 15 | typedef struct sqlcipher_provider sqlcipher_provider; 16 | typedef struct Btree Btree; 17 | 18 | typedef struct cipher_ctx { 19 | int store_pass; 20 | int derive_key; 21 | int kdf_iter; 22 | int fast_kdf_iter; 23 | int key_sz; 24 | int iv_sz; 25 | int block_sz; 26 | int pass_sz; 27 | int reserve_sz; 28 | int hmac_sz; 29 | int keyspec_sz; 30 | unsigned int flags; 31 | unsigned char *key; 32 | unsigned char *hmac_key; 33 | unsigned char *pass; 34 | char *keyspec; 35 | sqlcipher_provider *provider_; 36 | void *provider_ctx; 37 | } cipher_ctx; 38 | 39 | typedef struct codec_ctx { 40 | int kdf_salt_sz; 41 | int page_sz; 42 | unsigned char *kdf_salt; 43 | unsigned char *hmac_kdf_salt; 44 | unsigned char *buffer; 45 | Btree *pBt; 46 | cipher_ctx *read_ctx; 47 | cipher_ctx *write_ctx; 48 | unsigned int skip_read_hmac; 49 | unsigned int need_kdf_salt; 50 | } codec_ctx; 51 | 52 | syscall::open:entry 53 | /pid == $target 54 | && substr(copyinstr(arg0), strlen(copyinstr(arg0)) - 3) == ".db"/ 55 | { 56 | self->path = copyinstr(arg0); 57 | printf("\nsqlcipher '%s'\n", self->path); 58 | trace("--------------------------------------------------------------------------------\n"); 59 | } 60 | 61 | pid$target:WCDB:sqlcipher_cipher_ctx_key_derive:entry 62 | { 63 | /* 64 | * Pointers holding userland address 65 | */ 66 | self->ctx_u = arg0; 67 | self->c_ctx_u = arg1; 68 | } 69 | 70 | pid$target:WCDB:sqlcipher_cipher_ctx_key_derive:return 71 | { 72 | /* 73 | * Copy userland memory to kernel, so that we can play with it. 74 | */ 75 | self->ctx = (codec_ctx *) copyin(self->ctx_u, sizeof(codec_ctx)); 76 | self->c_ctx = (cipher_ctx *) copyin(self->c_ctx_u, sizeof(cipher_ctx)); 77 | 78 | /* 79 | * This gives us the 32-byte raw key followed by the 16-byte salt. 80 | * The salt is also stored at the first 16 bytes of the respective 81 | * *.db file, which you can verify with the following command: 82 | * 83 | * xxd -p -l 16 -g 0 '/path/to/foo.db' 84 | * 85 | */ 86 | printf("PRAGMA key = \"%s\";\n", 87 | copyinstr((user_addr_t) self->c_ctx->keyspec, 88 | self->c_ctx->keyspec_sz)); 89 | 90 | printf("PRAGMA cipher_compatibility = 3;\n"); 91 | printf("PRAGMA kdf_iter = %d;\n", self->c_ctx->kdf_iter); 92 | printf("PRAGMA cipher_page_size = %d;\n", self->ctx->page_sz); 93 | 94 | trace("........................................\n"); 95 | 96 | self->ctx_u = 0; 97 | self->c_ctx_u = 0; 98 | self->ctx = 0; 99 | self->c_ctx = 0; 100 | } 101 | 102 | -------------------------------------------------------------------------------- /macos/eavesdropper.d: -------------------------------------------------------------------------------- 1 | #!/usr/sbin/dtrace -s 2 | 3 | #pragma D option quiet 4 | /* maximum length of each string */ 5 | #pragma D option strsize=8k 6 | 7 | BEGIN 8 | { 9 | insert = "INSERT INTO Chat_"; 10 | insert_or_replace = "INSERT OR REPLACE INTO Chat_"; 11 | fields = "(mesLocalID,mesSvrID,msgCreateTime,msgContent,msgStatus,msgImgStatus,messageType,mesDes,msgSource,IntRes1,IntRes2,StrRes1,StrRes2,msgVoiceText,msgSeq,ConBlob) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)"; 12 | 13 | param[1] = "mesLocalID"; 14 | param[2] = "mesSvrID"; 15 | param[3] = "msgCreateTime"; 16 | param[4] = "msgContent"; 17 | param[5] = "msgStatus"; 18 | param[6] = "msgImgStatus"; 19 | param[7] = "messageType"; 20 | param[8] = "mesDes"; 21 | param[9] = "msgSource"; 22 | param[10] = "IntRes1"; 23 | param[11] = "IntRes2"; 24 | param[12] = "StrRes1"; 25 | param[13] = "StrRes2"; 26 | param[14] = "msgVoiceText"; 27 | param[15] = "msgSeq"; 28 | param[16] = "ConBlob"; 29 | } 30 | 31 | pid$target:WCDB:sqlite3_prepare_v2:entry 32 | /index(copyinstr(arg1), insert) == 0 33 | && index(copyinstr(arg1), fields) == 49/ 34 | { 35 | trace("\n================ INSERT ================\n"); 36 | self->in_prepare = 1; 37 | self->ppStmt = arg3; 38 | trace(copyinstr(arg1 + 17, 32)); 39 | trace("\n----------------------------------------\n"); 40 | } 41 | 42 | pid$target:WCDB:sqlite3_prepare_v2:entry 43 | /index(copyinstr(arg1), insert_or_replace) == 0 44 | && index(copyinstr(arg1), fields) == 60/ 45 | { 46 | trace("\n============ INSERT|REPLACE ============\n"); 47 | self->in_prepare = 1; 48 | self->ppStmt = arg3; 49 | trace(copyinstr(arg1 + 28, 32)); 50 | trace("\n----------------------------------------\n"); 51 | } 52 | 53 | pid$target:WCDB:sqlite3_prepare_v2:return 54 | /self->in_prepare == 1/ 55 | { 56 | self->in_prepare = 0; 57 | self->stmt = *((user_addr_t *) copyin(self->ppStmt, 8)); 58 | } 59 | 60 | pid$target:WCDB:sqlite3_bind_null:entry 61 | /arg0 == self->stmt && arg1 <= 16/ 62 | { 63 | printf("%s: NULL\n", param[arg1]); 64 | } 65 | 66 | pid$target:WCDB:sqlite3_bind_int64:entry 67 | /arg0 == self->stmt && arg1 <= 16/ 68 | { 69 | printf("%s: %d\n", param[arg1], arg2); 70 | } 71 | 72 | pid$target:WCDB:sqlite3_bind_text:entry 73 | /arg0 == self->stmt && arg1 <= 16/ 74 | { 75 | printf("%s: '%s'\n", param[arg1], arg2==0 ? "" : stringof(copyinstr(arg2))); 76 | } 77 | 78 | pid$target:WCDB:sqlite3_bind_blob:entry 79 | /arg0 == self->stmt && arg1 <= 16/ 80 | { 81 | printf("%s: See below for first 0x40 out of 0x%X bytes\n", param[arg1], arg3); 82 | tracemem(copyin(arg2, arg3), 0x40); 83 | } 84 | 85 | -------------------------------------------------------------------------------- /pcbakchat/gather.d: -------------------------------------------------------------------------------- 1 | #!/usr/sbin/dtrace -s 2 | 3 | #pragma D option quiet 4 | 5 | 6 | /* Crypto Key */ 7 | 8 | pid$target:libcommonCrypto.dylib:CCCrypt:entry 9 | /arg0 == 1 && arg1 == 4/ 10 | { 11 | self->buf = arg0 == 0 ? arg6 : arg8; 12 | self->buf_size = arg0 == 0 ? arg7 : arg9; 13 | self->key = copyin(arg3, arg4); 14 | printf("[%d] op = %d, alg = %d, buf_size = %d\n", 15 | tid, arg0, arg1, self->buf_size); 16 | tracemem(self->key, 32); 17 | } 18 | 19 | 20 | /* Database File Access (adapted from `macos/dbcracker.d`) */ 21 | 22 | typedef struct sqlcipher_provider sqlcipher_provider; 23 | typedef struct Btree Btree; 24 | 25 | typedef struct cipher_ctx { 26 | int store_pass; 27 | int derive_key; 28 | int kdf_iter; 29 | int fast_kdf_iter; 30 | int key_sz; 31 | int iv_sz; 32 | int block_sz; 33 | int pass_sz; 34 | int reserve_sz; 35 | int hmac_sz; 36 | int keyspec_sz; 37 | unsigned int flags; 38 | unsigned char *key; 39 | unsigned char *hmac_key; 40 | unsigned char *pass; 41 | char *keyspec; 42 | sqlcipher_provider *provider_; 43 | void *provider_ctx; 44 | } cipher_ctx; 45 | 46 | typedef struct codec_ctx { 47 | int kdf_salt_sz; 48 | int page_sz; 49 | unsigned char *kdf_salt; 50 | unsigned char *hmac_kdf_salt; 51 | unsigned char *buffer; 52 | Btree *pBt; 53 | cipher_ctx *read_ctx; 54 | cipher_ctx *write_ctx; 55 | unsigned int skip_read_hmac; 56 | unsigned int need_kdf_salt; 57 | } codec_ctx; 58 | 59 | syscall::open:entry 60 | /pid == $target && strstr(basename(copyinstr(arg0)), "Backup.db") != 0/ 61 | { 62 | self->path = copyinstr(arg0); 63 | printf("\nsqlcipher '%s'\n", self->path); 64 | trace("--------------------------------------------------------------------------------\n"); 65 | } 66 | 67 | pid$target:WCDB:sqlcipher_cipher_ctx_key_derive:entry 68 | { 69 | self->ctx_u = arg0; 70 | self->c_ctx_u = arg1; 71 | } 72 | 73 | pid$target:WCDB:sqlcipher_cipher_ctx_key_derive:return 74 | { 75 | self->ctx = (codec_ctx *) copyin(self->ctx_u, sizeof(codec_ctx)); 76 | self->c_ctx = (cipher_ctx *) copyin(self->c_ctx_u, sizeof(cipher_ctx)); 77 | 78 | printf("PRAGMA key = \"%s\";\n", 79 | copyinstr((user_addr_t) self->c_ctx->keyspec, 80 | self->c_ctx->keyspec_sz)); 81 | 82 | printf("PRAGMA cipher_compatibility = 3;\n"); 83 | printf("PRAGMA kdf_iter = %d;\n", self->c_ctx->kdf_iter); 84 | printf("PRAGMA cipher_page_size = %d;\n", self->ctx->page_sz); 85 | 86 | trace("........................................\n"); 87 | 88 | self->ctx_u = 0; 89 | self->c_ctx_u = 0; 90 | self->ctx = 0; 91 | self->c_ctx = 0; 92 | } 93 | 94 | 95 | /* Backup File Access */ 96 | /* FIXME: what about "BAK_1_TEXT" or "BAK_1_MEDIA"? */ 97 | 98 | syscall::open:entry 99 | /pid == $target && strstr(basename(copyinstr(arg0)), "BAK_0_TEXT") != 0/ 100 | { 101 | self->text = arg0; 102 | } 103 | 104 | syscall::open:entry 105 | /pid == $target && strstr(basename(copyinstr(arg0)), "BAK_0_MEDIA") != 0/ 106 | { 107 | self->media = arg0; 108 | } 109 | 110 | syscall::open:return 111 | /pid == $target && self->text/ 112 | { 113 | printf("[%d] OPEN %s as %d\n", tid, copyinstr(self->text), arg1); 114 | self->text = 0; 115 | self->text_fd = arg1; 116 | } 117 | 118 | syscall::open:return 119 | /pid == $target && self->media/ 120 | { 121 | printf("[%d] OPEN %s as %d\n", tid, copyinstr(self->media), arg1); 122 | self->media = 0; 123 | self->media_fd = arg1; 124 | } 125 | 126 | syscall::close:entry 127 | /pid == $target && arg0 == self->text_fd/ 128 | { 129 | printf("[%d] CLOSE TEXT %d\n", tid, self->text_fd); 130 | } 131 | 132 | syscall::close:entry 133 | /pid == $target && arg0 == self->media_fd/ 134 | { 135 | printf("[%d] CLOSE MEDIA %d\n", tid, self->text_fd); 136 | } 137 | 138 | 139 | /* Backup File I/O */ 140 | 141 | syscall::write:entry 142 | /pid == $target && arg0 == self->text_fd/ 143 | { 144 | printf("[%d] WRITE %d bytes to TEXT %d\n", tid, arg2, arg0); 145 | } 146 | 147 | syscall::write:entry 148 | /pid == $target && arg0 == self->media_fd/ 149 | { 150 | printf("[%d] WRITE %d bytes to MEDIA %d\n", tid, arg2, arg0); 151 | } 152 | -------------------------------------------------------------------------------- /pcbakchat/proto/BakChatMsgList.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | message BakChatMsgList { 4 | required uint32 count = 1; 5 | repeated BakChatMsgItem list = 2; 6 | } 7 | 8 | message BakChatMsgItem { 9 | required uint32 type = 1; 10 | optional string clientMsgId = 2; 11 | required SKBuiltinString_t fromUserName = 3; 12 | required SKBuiltinString_t toUserName = 4; 13 | required SKBuiltinString_t content = 5; 14 | required uint32 msgStatus = 6; 15 | required uint32 clientMsgTime = 7; 16 | optional string msgSource = 8; 17 | required uint32 msgId = 9; 18 | optional uint32 mediaIdCount = 10; 19 | repeated SKBuiltinString_t mediaId = 11; 20 | repeated SKBuiltinUint32_t mediaType = 12; 21 | optional SKBuiltinBuffer_t buffer = 13; 22 | optional uint32 bufferLength = 14; 23 | optional uint32 bufferType = 15; 24 | optional uint64 newMsgId = 16; 25 | optional uint32 sequentId = 17; 26 | optional int64 clientMsgMillTime = 18; 27 | optional uint32 msgFlag = 19; 28 | } 29 | 30 | message SKBuiltinString_t { 31 | optional string string = 1; 32 | } 33 | 34 | message SKBuiltinUint32_t { 35 | required uint32 uiVal = 1; 36 | } 37 | 38 | message SKBuiltinBuffer_t { 39 | required uint32 iLen = 1; 40 | optional bytes buffer = 2; 41 | } 42 | 43 | -------------------------------------------------------------------------------- /pcbakchat/usage.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nalzok/wechat-decipher-macos/f517dfc5e6c10981eef4303a9be84d610603836b/pcbakchat/usage.md --------------------------------------------------------------------------------