├── .gitignore ├── CNAME ├── LICENSE ├── Makefile ├── Package.resolved ├── Package.swift ├── README.md └── Sources ├── WeChatTweak-CLI ├── Command.swift └── main.swift └── insert_dylib ├── insert_dylib.h └── module.modulemap /.gitignore: -------------------------------------------------------------------------------- 1 | ### macOS ### 2 | # General 3 | .DS_Store 4 | .AppleDouble 5 | .LSOverride 6 | 7 | # Icon must end with two \r 8 | Icon 9 | 10 | 11 | # Thumbnails 12 | ._* 13 | 14 | # Files that might appear in the root of a volume 15 | .DocumentRevisions-V100 16 | .fseventsd 17 | .Spotlight-V100 18 | .TemporaryItems 19 | .Trashes 20 | .VolumeIcon.icns 21 | .com.apple.timemachine.donotpresent 22 | 23 | # Directories potentially created on remote AFP share 24 | .AppleDB 25 | .AppleDesktop 26 | Network Trash Folder 27 | Temporary Items 28 | .apdisk 29 | 30 | ### Windows ### 31 | # Windows thumbnail cache files 32 | Thumbs.db 33 | Thumbs.db:encryptable 34 | ehthumbs.db 35 | ehthumbs_vista.db 36 | 37 | # Dump file 38 | *.stackdump 39 | 40 | # Folder config file 41 | [Dd]esktop.ini 42 | 43 | # Recycle Bin used on file shares 44 | $RECYCLE.BIN/ 45 | 46 | # Windows Installer files 47 | *.cab 48 | *.msi 49 | *.msix 50 | *.msm 51 | *.msp 52 | 53 | # Windows shortcuts 54 | *.lnk 55 | 56 | ### Xcode ### 57 | # Xcode 58 | # 59 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 60 | 61 | ## User settings 62 | xcuserdata/ 63 | 64 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 65 | *.xcscmblueprint 66 | *.xccheckout 67 | 68 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 69 | build/ 70 | DerivedData/ 71 | *.moved-aside 72 | *.pbxuser 73 | !default.pbxuser 74 | *.mode1v3 75 | !default.mode1v3 76 | *.mode2v3 77 | !default.mode2v3 78 | *.perspectivev3 79 | !default.perspectivev3 80 | 81 | ## Gcc Patch 82 | /*.gcno 83 | 84 | ### Xcode Patch ### 85 | *.xcodeproj/* 86 | !*.xcodeproj/project.pbxproj 87 | !*.xcodeproj/xcshareddata/ 88 | !*.xcworkspace/contents.xcworkspacedata 89 | **/xcshareddata/WorkspaceSettings.xcsettings 90 | 91 | ### SwiftPackageManager ### 92 | Packages 93 | .build/ 94 | .swiftpm/ 95 | xcuserdata 96 | DerivedData/ 97 | *.xcodeproj 98 | 99 | ### Release ### 100 | 101 | /wechattweak-cli 102 | -------------------------------------------------------------------------------- /CNAME: -------------------------------------------------------------------------------- 1 | cli.tweaks.app -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .DEFAULT := build 2 | 3 | build: 4 | swift package purge-cache 5 | swift package clean 6 | swift build -c release --arch arm64 --arch x86_64 7 | cp .build/apple/Products/Release/wechattweak-cli . 8 | 9 | clean: 10 | swift package purge-cache 11 | swift package clean 12 | rm -rf .build wechattweak-cli 13 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "Alamofire", 6 | "repositoryURL": "https://github.com/Alamofire/Alamofire", 7 | "state": { 8 | "branch": null, 9 | "revision": "bc268c28fb170f494de9e9927c371b8342979ece", 10 | "version": "5.7.1" 11 | } 12 | }, 13 | { 14 | "package": "PromiseKit", 15 | "repositoryURL": "https://github.com/mxcl/PromiseKit", 16 | "state": { 17 | "branch": null, 18 | "revision": "8a98e31a47854d3180882c8068cc4d9381bf382d", 19 | "version": "6.22.1" 20 | } 21 | }, 22 | { 23 | "package": "swift-argument-parser", 24 | "repositoryURL": "https://github.com/apple/swift-argument-parser", 25 | "state": { 26 | "branch": null, 27 | "revision": "fee6933f37fde9a5e12a1e4aeaa93fe60116ff2a", 28 | "version": "1.2.2" 29 | } 30 | } 31 | ] 32 | }, 33 | "version": 1 34 | } 35 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.5 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "WeChatTweak-CLI", 7 | platforms: [ 8 | .macOS(.v10_12) 9 | ], 10 | products: [ 11 | .executable( 12 | name: "wechattweak-cli", 13 | targets: [ 14 | "WeChatTweak-CLI" 15 | ] 16 | ) 17 | ], 18 | dependencies: [ 19 | .package(url: "https://github.com/Alamofire/Alamofire", from: "5.7.1"), 20 | .package(url: "https://github.com/mxcl/PromiseKit", from: "6.22.1"), 21 | .package(url: "https://github.com/apple/swift-argument-parser", from: "1.2.2") 22 | ], 23 | targets: [ 24 | .executableTarget( 25 | name: "WeChatTweak-CLI", 26 | dependencies: [ 27 | "Alamofire", 28 | "PromiseKit", 29 | "insert_dylib", 30 | .product(name: "ArgumentParser", package: "swift-argument-parser") 31 | ] 32 | ), 33 | .systemLibrary( 34 | name: "insert_dylib", 35 | path: "Sources/insert_dylib" 36 | ) 37 | ] 38 | ) 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WeChatTweak-CLI 2 | 3 | A command line utility to work with WeChatTweak-macOS. 4 | 5 | ## Overview 6 | 7 | ```bash 8 | OVERVIEW: A command line utility to work with WeChatTweak-macOS. 9 | 10 | USAGE: wechattweak-cli 11 | 12 | OPTIONS: 13 | -h, --help Show help information. 14 | 15 | SUBCOMMANDS: 16 | install Install or upgrade tweak. 17 | uninstall Uninstall tweak. 18 | resign Force resign WeChat.app 19 | version Get current version of WeChatTweak. 20 | 21 | See 'wechattweak-cli help ' for detailed help. 22 | ``` 23 | 24 | ## Requirements 25 | 26 | - macOS >= 10.12 27 | - Swift 5 Runtime Support 28 | 29 | ## Install 30 | 31 | ### Homebrew 32 | 33 | You can install [WeChatTweak-CLI](https://github.com/sunnyyoung/WeChatTweak-CLI) via Homebrew. 34 | 35 | ```bash 36 | $ brew install sunnyyoung/repo/wechattweak-cli 37 | ``` 38 | 39 | ### Manual (**NOT RECOMMENDED**) 40 | 41 | 1. Download the [WeChatTweak-CLI](https://github.com/sunnyyoung/WeChatTweak-CLI/releases/latest/download/wechattweak-cli) 42 | 2. Remove file attributes: `xattr -d com.apple.quarantine wechattweak-cli` 43 | 3. Make sure the binary executable: `chmod +x wechattweak-cli` 44 | 4. Run: `sudo ./wechattweak-cli install` 45 | 46 | ## License 47 | 48 | The [Apache License 2.0](LICENSE). 49 | -------------------------------------------------------------------------------- /Sources/WeChatTweak-CLI/Command.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Command.swift 3 | // 4 | // Created by Sunny Young. 5 | // 6 | 7 | import Foundation 8 | import Alamofire 9 | import PromiseKit 10 | import ArgumentParser 11 | import insert_dylib 12 | 13 | struct Command { 14 | static func check() -> Promise { 15 | return getuid() == 0 ? .value(()) : .init(error: CLIError.permission) 16 | } 17 | 18 | static func cleanup() -> Guarantee { 19 | return Guarantee { seal in 20 | try? FileManager.default.removeItem(atPath: Temp.zip) 21 | try? FileManager.default.removeItem(atPath: Temp.binary) 22 | seal(()) 23 | } 24 | } 25 | 26 | static func backup() -> Promise { 27 | print("------ Backup ------") 28 | return Promise { seal in 29 | do { 30 | if FileManager.default.fileExists(atPath: App.backup) { 31 | print("WeChat.bak exists, skip it...") 32 | } else { 33 | try FileManager.default.copyItem(atPath: App.binary, toPath: App.backup) 34 | print("Created WeChat.bak...") 35 | } 36 | seal.fulfill(()) 37 | } catch { 38 | seal.reject(error) 39 | } 40 | } 41 | } 42 | 43 | static func restore() -> Promise { 44 | print("------ Restore ------") 45 | return Promise { seal in 46 | do { 47 | if FileManager.default.fileExists(atPath: App.backup) { 48 | try FileManager.default.removeItem(atPath: App.binary) 49 | try FileManager.default.moveItem(atPath: App.backup, toPath: App.binary) 50 | try? FileManager.default.removeItem(atPath: App.framework) 51 | print("Restored WeChat...") 52 | } else { 53 | print("WeChat.bak not exists, skip it...") 54 | } 55 | seal.fulfill(()) 56 | } catch { 57 | seal.reject(error) 58 | } 59 | } 60 | } 61 | 62 | static func download() -> Promise { 63 | print("------ Download ------") 64 | return Promise { seal in 65 | let destination: DownloadRequest.Destination = { _, _ in 66 | return (.init(fileURLWithPath: Temp.zip), [.removePreviousFile]) 67 | } 68 | AF.download(Constant.url, to: destination).response { response in 69 | if let error = response.error { 70 | seal.reject(CLIError.downloading(error)) 71 | } else { 72 | seal.fulfill(()) 73 | } 74 | } 75 | } 76 | } 77 | 78 | static func unzip() -> Promise { 79 | print("------ Unzip ------") 80 | return Command.execute(command: "rm -rf \(App.framework); unzip \(Temp.zip) -d \(App.macos)") 81 | } 82 | 83 | static func insert() -> Promise { 84 | print("------ Insert Dylib ------") 85 | return Promise { seal in 86 | insert_dylib.insert("@executable_path/WeChatTweak.framework/WeChatTweak", App.binary) == EXIT_SUCCESS ? seal.fulfill(()) : seal.reject(CLIError.insertDylib) 87 | } 88 | } 89 | 90 | static func removeCodesign() -> Promise { 91 | print("------ Remove Codesign ------") 92 | return Command.execute(command: "codesign --remove-sign \(App.binary)") 93 | } 94 | 95 | static func addCodesign() -> Promise { 96 | print("------ Add Codesign ------") 97 | return Command.execute(command: "codesign --force --deep --sign - \(App.binary)") 98 | } 99 | 100 | static func resetPermission() -> Promise { 101 | print("------ Reset ScreenCapture privacy permission ------") 102 | return Command.execute(command: "tccutil reset ScreenCapture com.tencent.xinWeChat") 103 | } 104 | 105 | private static func execute(command: String) -> Promise { 106 | return Promise { seal in 107 | print("Execute command: \(command)") 108 | var error: NSDictionary? 109 | guard let script = NSAppleScript(source: "do shell script \"\(command)\"") else { 110 | return seal.reject(CLIError.executing(command: command, error: ["error": "Create script failed."])) 111 | } 112 | script.executeAndReturnError(&error) 113 | if let error = error { 114 | seal.reject(CLIError.executing(command: command, error: error)) 115 | } else { 116 | seal.fulfill(()) 117 | } 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /Sources/WeChatTweak-CLI/main.swift: -------------------------------------------------------------------------------- 1 | // 2 | // main.swift 3 | // 4 | // Created by Sunny Young. 5 | // 6 | 7 | import Foundation 8 | import PromiseKit 9 | import ArgumentParser 10 | 11 | struct Constant { 12 | static let url = URL(string: "https://github.com/Sunnyyoung/WeChatTweak-macOS/releases/latest/download/WeChatTweak.framework.zip")! 13 | } 14 | 15 | struct App { 16 | static let root = "/Applications" 17 | static let app = root.appending("/WeChat.app") 18 | static let macos = app.appending("/Contents/MacOS") 19 | static let binary = app.appending("/Contents/MacOS/WeChat") 20 | static let backup = app.appending("/Contents/MacOS/WeChat.bak") 21 | 22 | static let framework = macos.appending("/WeChatTweak.framework") 23 | } 24 | 25 | struct Temp { 26 | static let root = "/tmp" 27 | static let binary = root.appending("/WeChat") 28 | static let zip = root.appending("/WeChatTweak.zip") 29 | } 30 | 31 | enum CLIError: LocalizedError { 32 | case permission 33 | case downloading(Error) 34 | case insertDylib 35 | case executing(command: String, error: NSDictionary) 36 | 37 | var errorDescription: String? { 38 | switch self { 39 | case .permission: 40 | return "Please run with `sudo`." 41 | case let .downloading(error): 42 | return "Download failed with error: \(error)" 43 | case .insertDylib: 44 | return "Insert dylib failed" 45 | case let .executing(command, error): 46 | return "Execute command: \(command) failed: \(error)" 47 | } 48 | } 49 | } 50 | 51 | struct Install: ParsableCommand { 52 | static var configuration = CommandConfiguration(abstract: "Install or upgrade tweak.") 53 | 54 | func run() throws { 55 | firstly { 56 | Command.check() 57 | }.then { 58 | Command.cleanup() 59 | }.then { 60 | Command.download() 61 | }.then { 62 | Command.unzip() 63 | }.then { 64 | Command.backup() 65 | }.then { 66 | Command.removeCodesign() 67 | }.then { 68 | Command.insert() 69 | }.then { 70 | Command.addCodesign() 71 | }.then { 72 | Command.resetPermission() 73 | }.done { 74 | print("Install success!") 75 | }.catch { error in 76 | print("Install failed: \(error.localizedDescription)") 77 | }.finally { 78 | CFRunLoopStop(CFRunLoopGetCurrent()) 79 | } 80 | } 81 | } 82 | 83 | struct Uninstall: ParsableCommand { 84 | static var configuration = CommandConfiguration(abstract: "Uninstall tweak.") 85 | 86 | func run() throws { 87 | firstly { 88 | Command.check() 89 | }.then { 90 | Command.cleanup() 91 | }.then { 92 | Command.restore() 93 | }.done { 94 | print("Uninstall success!") 95 | }.catch { error in 96 | print("Uninstall failed: \(error)") 97 | }.finally { 98 | CFRunLoopStop(CFRunLoopGetCurrent()) 99 | } 100 | } 101 | } 102 | 103 | struct Version: ParsableCommand { 104 | static var configuration = CommandConfiguration(abstract: "Get current version of WeChatTweak.") 105 | 106 | func run() throws { 107 | return firstly { 108 | getVersion() 109 | }.done { version in 110 | print(version ?? "Unknown") 111 | }.catch { error in 112 | print("Get version failed: \(error)") 113 | }.finally { 114 | CFRunLoopStop(CFRunLoopGetCurrent()) 115 | } 116 | } 117 | 118 | func getVersion() -> Promise { 119 | return Promise { seal in 120 | do { 121 | let data = try Data(contentsOf: URL(fileURLWithPath: App.framework.appending("/Versions/A/Resources/Info.plist"))) 122 | let info = try PropertyListSerialization.propertyList(from: data, options: [], format: nil) 123 | seal.fulfill((info as? [String: Any])?["CFBundleShortVersionString"] as? String) 124 | } catch { 125 | seal.reject(error) 126 | } 127 | } 128 | } 129 | } 130 | 131 | struct Resign: ParsableCommand { 132 | static var configuration = CommandConfiguration(abstract: "Force resign WeChat.app") 133 | 134 | func run() throws { 135 | firstly { 136 | Command.removeCodesign() 137 | }.then { 138 | Command.addCodesign() 139 | }.catch { error in 140 | print("Resign failed: \(error.localizedDescription)") 141 | }.finally { 142 | CFRunLoopStop(CFRunLoopGetCurrent()) 143 | } 144 | } 145 | } 146 | 147 | struct Tweak: ParsableCommand { 148 | static var configuration = CommandConfiguration( 149 | commandName: "wechattweak-cli", 150 | abstract: "A command line utility to work with WeChatTweak-macOS.", 151 | subcommands: [ 152 | Install.self, 153 | Uninstall.self, 154 | Resign.self, 155 | Version.self 156 | ], 157 | defaultSubcommand: Self.self 158 | ) 159 | } 160 | 161 | defer { 162 | try? FileManager.default.removeItem(atPath: Temp.zip) 163 | try? FileManager.default.removeItem(atPath: Temp.binary) 164 | } 165 | 166 | Tweak.main() 167 | CFRunLoopRun() 168 | -------------------------------------------------------------------------------- /Sources/insert_dylib/insert_dylib.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | #define IS_64_BIT(x) ((x) == MH_MAGIC_64 || (x) == MH_CIGAM_64) 15 | #define IS_LITTLE_ENDIAN(x) ((x) == FAT_CIGAM || (x) == MH_CIGAM_64 || (x) == MH_CIGAM) 16 | #define SWAP32(x, magic) (IS_LITTLE_ENDIAN(magic)? OSSwapInt32(x): (x)) 17 | #define SWAP64(x, magic) (IS_LITTLE_ENDIAN(magic)? OSSwapInt64(x): (x)) 18 | 19 | #define ROUND_UP(x, y) (((x) + (y) - 1) & -(y)) 20 | 21 | #define ABSDIFF(x, y) ((x) > (y)? (uintmax_t)(x) - (uintmax_t)(y): (uintmax_t)(y) - (uintmax_t)(x)) 22 | 23 | #define BUFSIZE 512 24 | 25 | void fbzero(FILE *f, off_t offset, size_t len) { 26 | static unsigned char zeros[BUFSIZE] = {0}; 27 | fseeko(f, offset, SEEK_SET); 28 | while(len != 0) { 29 | size_t size = MIN(len, sizeof(zeros)); 30 | fwrite(zeros, size, 1, f); 31 | len -= size; 32 | } 33 | } 34 | 35 | void fmemmove(FILE *f, off_t dst, off_t src, size_t len) { 36 | static unsigned char buf[BUFSIZE]; 37 | while(len != 0) { 38 | size_t size = MIN(len, sizeof(buf)); 39 | fseeko(f, src, SEEK_SET); 40 | fread(&buf, size, 1, f); 41 | fseeko(f, dst, SEEK_SET); 42 | fwrite(buf, size, 1, f); 43 | 44 | len -= size; 45 | src += size; 46 | dst += size; 47 | } 48 | } 49 | 50 | int weak_flag = false; 51 | 52 | size_t fpeek(void *restrict ptr, size_t size, size_t nitems, FILE *restrict stream) { 53 | off_t pos = ftello(stream); 54 | size_t result = fread(ptr, size, nitems, stream); 55 | fseeko(stream, pos, SEEK_SET); 56 | return result; 57 | } 58 | 59 | void *read_load_command(FILE *f, uint32_t cmdsize) { 60 | void *lc = malloc(cmdsize); 61 | 62 | fpeek(lc, cmdsize, 1, f); 63 | 64 | return lc; 65 | } 66 | 67 | bool check_load_commands(FILE *f, struct mach_header *mh, size_t header_offset, size_t commands_offset, const char *dylib_path, off_t *slice_size) { 68 | fseeko(f, commands_offset, SEEK_SET); 69 | 70 | uint32_t ncmds = SWAP32(mh->ncmds, mh->magic); 71 | 72 | off_t linkedit_32_pos = -1; 73 | off_t linkedit_64_pos = -1; 74 | struct segment_command linkedit_32; 75 | struct segment_command_64 linkedit_64; 76 | 77 | off_t symtab_pos = -1; 78 | uint32_t symtab_size = 0; 79 | 80 | for(int i = 0; i < ncmds; i++) { 81 | struct load_command lc; 82 | fpeek(&lc, sizeof(lc), 1, f); 83 | 84 | uint32_t cmdsize = SWAP32(lc.cmdsize, mh->magic); 85 | uint32_t cmd = SWAP32(lc.cmd, mh->magic); 86 | 87 | switch(cmd) { 88 | case LC_CODE_SIGNATURE: 89 | if(i == ncmds - 1) { 90 | printf("LC_CODE_SIGNATURE load command found. Remove it.\n"); 91 | 92 | struct linkedit_data_command *cmd = read_load_command(f, cmdsize); 93 | 94 | fbzero(f, ftello(f), cmdsize); 95 | 96 | uint32_t dataoff = SWAP32(cmd->dataoff, mh->magic); 97 | uint32_t datasize = SWAP32(cmd->datasize, mh->magic); 98 | 99 | free(cmd); 100 | 101 | uint64_t linkedit_fileoff = 0; 102 | uint64_t linkedit_filesize = 0; 103 | 104 | if(linkedit_32_pos != -1) { 105 | linkedit_fileoff = SWAP32(linkedit_32.fileoff, mh->magic); 106 | linkedit_filesize = SWAP32(linkedit_32.filesize, mh->magic); 107 | } else if(linkedit_64_pos != -1) { 108 | linkedit_fileoff = SWAP64(linkedit_64.fileoff, mh->magic); 109 | linkedit_filesize = SWAP64(linkedit_64.filesize, mh->magic); 110 | } else { 111 | fprintf(stderr, "Warning: __LINKEDIT segment not found.\n"); 112 | } 113 | 114 | if(linkedit_32_pos != -1 || linkedit_64_pos != -1) { 115 | if(linkedit_fileoff + linkedit_filesize != *slice_size) { 116 | fprintf(stderr, "Warning: __LINKEDIT segment is not at the end of the file, so codesign will not work on the patched binary.\n"); 117 | } else { 118 | if(dataoff + datasize != *slice_size) { 119 | fprintf(stderr, "Warning: Codesignature is not at the end of __LINKEDIT segment, so codesign will not work on the patched binary.\n"); 120 | } else { 121 | *slice_size -= datasize; 122 | //int64_t diff_size = 0; 123 | if(symtab_pos == -1) { 124 | fprintf(stderr, "Warning: LC_SYMTAB load command not found. codesign might not work on the patched binary.\n"); 125 | } else { 126 | fseeko(f, symtab_pos, SEEK_SET); 127 | struct symtab_command *symtab = read_load_command(f, symtab_size); 128 | 129 | uint32_t strsize = SWAP32(symtab->strsize, mh->magic); 130 | int64_t diff_size = SWAP32(symtab->stroff, mh->magic) + strsize - (int64_t)*slice_size; 131 | if(-0x10 <= diff_size && diff_size <= 0) { 132 | symtab->strsize = SWAP32((uint32_t)(strsize - diff_size), mh->magic); 133 | fwrite(symtab, symtab_size, 1, f); 134 | } else { 135 | fprintf(stderr, "Warning: String table doesn't appear right before code signature. codesign might not work on the patched binary. (0x%llx)\n", diff_size); 136 | } 137 | 138 | free(symtab); 139 | } 140 | 141 | linkedit_filesize -= datasize; 142 | uint64_t linkedit_vmsize = ROUND_UP(linkedit_filesize, 0x1000); 143 | 144 | if(linkedit_32_pos != -1) { 145 | linkedit_32.filesize = SWAP32((uint32_t)linkedit_filesize, mh->magic); 146 | linkedit_32.vmsize = SWAP32((uint32_t)linkedit_vmsize, mh->magic); 147 | 148 | fseeko(f, linkedit_32_pos, SEEK_SET); 149 | fwrite(&linkedit_32, sizeof(linkedit_32), 1, f); 150 | } else { 151 | linkedit_64.filesize = SWAP64(linkedit_filesize, mh->magic); 152 | linkedit_64.vmsize = SWAP64(linkedit_vmsize, mh->magic); 153 | 154 | fseeko(f, linkedit_64_pos, SEEK_SET); 155 | fwrite(&linkedit_64, sizeof(linkedit_64), 1, f); 156 | } 157 | 158 | goto fix_header; 159 | } 160 | } 161 | } 162 | 163 | // If we haven't truncated the file, zero out the code signature 164 | fbzero(f, header_offset + dataoff, datasize); 165 | 166 | fix_header: 167 | mh->ncmds = SWAP32(ncmds - 1, mh->magic); 168 | mh->sizeofcmds = SWAP32(SWAP32(mh->sizeofcmds, mh->magic) - cmdsize, mh->magic); 169 | 170 | return true; 171 | } else { 172 | printf("LC_CODE_SIGNATURE is not the last load command, so couldn't remove.\n"); 173 | } 174 | break; 175 | case LC_LOAD_DYLIB: 176 | case LC_LOAD_WEAK_DYLIB: { 177 | struct dylib_command *dylib_command = read_load_command(f, cmdsize); 178 | 179 | union lc_str offset = dylib_command->dylib.name; 180 | char *name = &((char *)dylib_command)[SWAP32(offset.offset, mh->magic)]; 181 | 182 | int cmp = strcmp(name, dylib_path); 183 | 184 | free(dylib_command); 185 | 186 | if(cmp == 0) { 187 | printf("Binary already contains a load command for that dylib. Skip it.\n"); 188 | return false; 189 | } 190 | 191 | break; 192 | } 193 | case LC_SEGMENT: 194 | case LC_SEGMENT_64: 195 | if(cmd == LC_SEGMENT) { 196 | struct segment_command *cmd = read_load_command(f, cmdsize); 197 | if(strcmp(cmd->segname, "__LINKEDIT") == 0) { 198 | linkedit_32_pos = ftello(f); 199 | linkedit_32 = *cmd; 200 | } 201 | free(cmd); 202 | } else { 203 | struct segment_command_64 *cmd = read_load_command(f, cmdsize); 204 | if(strcmp(cmd->segname, "__LINKEDIT") == 0) { 205 | linkedit_64_pos = ftello(f); 206 | linkedit_64 = *cmd; 207 | } 208 | free(cmd); 209 | } 210 | case LC_SYMTAB: 211 | symtab_pos = ftello(f); 212 | symtab_size = cmdsize; 213 | } 214 | 215 | fseeko(f, SWAP32(lc.cmdsize, mh->magic), SEEK_CUR); 216 | } 217 | 218 | return true; 219 | } 220 | 221 | bool _insert_dylib(FILE *f, size_t header_offset, const char *dylib_path, off_t *slice_size) { 222 | fseeko(f, header_offset, SEEK_SET); 223 | 224 | struct mach_header mh; 225 | fread(&mh, sizeof(struct mach_header), 1, f); 226 | 227 | if(mh.magic != MH_MAGIC_64 && mh.magic != MH_CIGAM_64 && mh.magic != MH_MAGIC && mh.magic != MH_CIGAM) { 228 | printf("Unknown magic: 0x%x\n", mh.magic); 229 | return false; 230 | } 231 | 232 | size_t commands_offset = header_offset + (IS_64_BIT(mh.magic)? sizeof(struct mach_header_64): sizeof(struct mach_header)); 233 | 234 | bool cont = check_load_commands(f, &mh, header_offset, commands_offset, dylib_path, slice_size); 235 | 236 | if(!cont) { 237 | return true; 238 | } 239 | 240 | // Even though a padding of 4 works for x86_64, codesign doesn't like it 241 | size_t path_padding = 8; 242 | 243 | size_t dylib_path_len = strlen(dylib_path); 244 | size_t dylib_path_size = (dylib_path_len & ~(path_padding - 1)) + path_padding; 245 | uint32_t cmdsize = (uint32_t)(sizeof(struct dylib_command) + dylib_path_size); 246 | 247 | struct dylib_command dylib_command = { 248 | .cmd = SWAP32(weak_flag? LC_LOAD_WEAK_DYLIB: LC_LOAD_DYLIB, mh.magic), 249 | .cmdsize = SWAP32(cmdsize, mh.magic), 250 | .dylib = { 251 | .name = SWAP32(sizeof(struct dylib_command), mh.magic), 252 | .timestamp = 0, 253 | .current_version = 0, 254 | .compatibility_version = 0 255 | } 256 | }; 257 | 258 | uint32_t sizeofcmds = SWAP32(mh.sizeofcmds, mh.magic); 259 | 260 | fseeko(f, commands_offset + sizeofcmds, SEEK_SET); 261 | char space[cmdsize]; 262 | 263 | fread(&space, cmdsize, 1, f); 264 | 265 | bool empty = true; 266 | for(int i = 0; i < cmdsize; i++) { 267 | if(space[i] != 0) { 268 | empty = false; 269 | break; 270 | } 271 | } 272 | 273 | if(!empty) { 274 | printf("It doesn't seem like there is enough empty space. Continue anyway."); 275 | } 276 | 277 | fseeko(f, -((off_t)cmdsize), SEEK_CUR); 278 | 279 | char *dylib_path_padded = calloc(dylib_path_size, 1); 280 | memcpy(dylib_path_padded, dylib_path, dylib_path_len); 281 | 282 | fwrite(&dylib_command, sizeof(dylib_command), 1, f); 283 | fwrite(dylib_path_padded, dylib_path_size, 1, f); 284 | 285 | free(dylib_path_padded); 286 | 287 | mh.ncmds = SWAP32(SWAP32(mh.ncmds, mh.magic) + 1, mh.magic); 288 | sizeofcmds += cmdsize; 289 | mh.sizeofcmds = SWAP32(sizeofcmds, mh.magic); 290 | 291 | fseeko(f, header_offset, SEEK_SET); 292 | fwrite(&mh, sizeof(mh), 1, f); 293 | 294 | return true; 295 | } 296 | 297 | int insert(const char *lib_path, const char *bin_path) { 298 | const char *lc_name = weak_flag? "LC_LOAD_WEAK_DYLIB": "LC_LOAD_DYLIB"; 299 | 300 | struct stat s; 301 | 302 | if(stat(bin_path, &s) != 0) { 303 | perror(bin_path); 304 | return EXIT_FAILURE; 305 | } 306 | 307 | if(lib_path[0] != '@' && stat(lib_path, &s) != 0) { 308 | return EXIT_FAILURE; 309 | } 310 | 311 | FILE *f = fopen(bin_path, "r+"); 312 | 313 | if(!f) { 314 | printf("Couldn't open file %s\n", bin_path); 315 | return EXIT_FAILURE; 316 | } 317 | 318 | bool success = true; 319 | 320 | fseeko(f, 0, SEEK_END); 321 | off_t file_size = ftello(f); 322 | rewind(f); 323 | 324 | uint32_t magic; 325 | fread(&magic, sizeof(uint32_t), 1, f); 326 | 327 | switch(magic) { 328 | case FAT_MAGIC: 329 | case FAT_CIGAM: { 330 | fseeko(f, 0, SEEK_SET); 331 | 332 | struct fat_header fh; 333 | fread(&fh, sizeof(fh), 1, f); 334 | 335 | uint32_t nfat_arch = SWAP32(fh.nfat_arch, magic); 336 | 337 | printf("Binary is a fat binary with %d archs.\n", nfat_arch); 338 | 339 | struct fat_arch archs[nfat_arch]; 340 | fread(archs, sizeof(archs), 1, f); 341 | 342 | int fails = 0; 343 | 344 | uint32_t offset = 0; 345 | if(nfat_arch > 0) { 346 | offset = SWAP32(archs[0].offset, magic); 347 | } 348 | 349 | for(int i = 0; i < nfat_arch; i++) { 350 | off_t orig_offset = SWAP32(archs[i].offset, magic); 351 | off_t orig_slice_size = SWAP32(archs[i].size, magic); 352 | offset = ROUND_UP(offset, 1 << SWAP32(archs[i].align, magic)); 353 | if(orig_offset != offset) { 354 | fmemmove(f, offset, orig_offset, orig_slice_size); 355 | fbzero(f, MIN(offset, orig_offset) + orig_slice_size, ABSDIFF(offset, orig_offset)); 356 | 357 | archs[i].offset = SWAP32(offset, magic); 358 | } 359 | 360 | off_t slice_size = orig_slice_size; 361 | bool r = _insert_dylib(f, offset, lib_path, &slice_size); 362 | if(!r) { 363 | printf("Failed to add %s to arch #%d!\n", lc_name, i + 1); 364 | fails++; 365 | } 366 | 367 | if(slice_size < orig_slice_size && i < nfat_arch - 1) { 368 | fbzero(f, offset + slice_size, orig_slice_size - slice_size); 369 | } 370 | 371 | file_size = offset + slice_size; 372 | offset += slice_size; 373 | archs[i].size = SWAP32((uint32_t)slice_size, magic); 374 | } 375 | 376 | rewind(f); 377 | fwrite(&fh, sizeof(fh), 1, f); 378 | fwrite(archs, sizeof(archs), 1, f); 379 | 380 | // We need to flush before truncating 381 | fflush(f); 382 | ftruncate(fileno(f), file_size); 383 | 384 | if(fails == 0) { 385 | printf("Added %s to all archs in %s\n", lc_name, bin_path); 386 | } else if(fails == nfat_arch) { 387 | printf("Failed to add %s to any archs.\n", lc_name); 388 | success = false; 389 | } else { 390 | printf("Added %s to %d/%d archs in %s\n", lc_name, nfat_arch - fails, nfat_arch, bin_path); 391 | } 392 | 393 | break; 394 | } 395 | case MH_MAGIC_64: 396 | case MH_CIGAM_64: 397 | case MH_MAGIC: 398 | case MH_CIGAM: 399 | if(_insert_dylib(f, 0, lib_path, &file_size)) { 400 | ftruncate(fileno(f), file_size); 401 | printf("Added %s to %s\n", lc_name, bin_path); 402 | } else { 403 | printf("Failed to add %s!\n", lc_name); 404 | success = false; 405 | } 406 | break; 407 | default: 408 | printf("Unknown magic: 0x%x\n", magic); 409 | return EXIT_FAILURE; 410 | } 411 | 412 | fclose(f); 413 | 414 | if(!success) { 415 | unlink(bin_path); 416 | return EXIT_FAILURE; 417 | } else { 418 | return EXIT_SUCCESS; 419 | } 420 | } 421 | -------------------------------------------------------------------------------- /Sources/insert_dylib/module.modulemap: -------------------------------------------------------------------------------- 1 | module insert_dylib { 2 | header "insert_dylib.h" 3 | export * 4 | } 5 | --------------------------------------------------------------------------------