├── .gitignore ├── Package.swift ├── Package.resolved ├── LICENSE ├── README.md ├── .github └── workflows │ └── release.yml ├── Tests └── PDGuessTests.swift └── Sources └── PDGuess.swift /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | xcuserdata/ 5 | DerivedData/ 6 | .swiftpm/configuration/registries.json 7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 8 | .netrc 9 | .vscode/ 10 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 6.0 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "swift-pd-guess", 8 | platforms: [.macOS(.v15)], 9 | products: [ 10 | .executable(name: "swift-pd-guess", targets: ["swift-pd-guess"]), 11 | ], 12 | dependencies: [ 13 | .package(url: "https://github.com/apple/swift-argument-parser", from: "1.2.0"), 14 | .package(url: "https://github.com/apple/swift-algorithms", from: "1.2.0"), 15 | ], 16 | targets: [ 17 | .executableTarget( 18 | name: "swift-pd-guess", 19 | dependencies: [ 20 | .product(name: "ArgumentParser", package: "swift-argument-parser"), 21 | .product(name: "Algorithms", package: "swift-algorithms"), 22 | ] 23 | ), 24 | .testTarget( 25 | name: "swift-pd-guess-tests", 26 | dependencies: ["swift-pd-guess"] 27 | ), 28 | ] 29 | ) 30 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "originHash" : "6007b602a0a2d8195309962f93d9d5c88497e7ff1b7ebd8c9a8cc20bab4e2a64", 3 | "pins" : [ 4 | { 5 | "identity" : "swift-algorithms", 6 | "kind" : "remoteSourceControl", 7 | "location" : "https://github.com/apple/swift-algorithms", 8 | "state" : { 9 | "revision" : "f6919dfc309e7f1b56224378b11e28bab5bccc42", 10 | "version" : "1.2.0" 11 | } 12 | }, 13 | { 14 | "identity" : "swift-argument-parser", 15 | "kind" : "remoteSourceControl", 16 | "location" : "https://github.com/apple/swift-argument-parser", 17 | "state" : { 18 | "revision" : "41982a3656a71c768319979febd796c6fd111d5c", 19 | "version" : "1.5.0" 20 | } 21 | }, 22 | { 23 | "identity" : "swift-numerics", 24 | "kind" : "remoteSourceControl", 25 | "location" : "https://github.com/apple/swift-numerics.git", 26 | "state" : { 27 | "revision" : "0a5bc04095a675662cf24757cc0640aa2204253b", 28 | "version" : "1.0.2" 29 | } 30 | } 31 | ], 32 | "version" : 3 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Kyle-Ye 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # swift-pd-guess 2 | 3 | Try to guess the file name from private discriminator of a Swift symbol. 4 | 5 | ## Usgae 6 | 7 | ```shell 8 | swift-pd-guess --prefix [--suffix ] [--max-count ] 9 | ``` 10 | 11 | ## Example 12 | 13 | For DemoKit binary, we can see some private symbol have `_00D91DAB7748ABA327DC94E69D678CAF` in their demangled name. 14 | 15 | Eg. `_$s7DemoKit3FooV7Private33_00D91DAB7748ABA327DC94E69D678CAFLLVMn` --demangle--> `nominal type descriptor for DemoKit.Foo.(Private in _00D91DAB7748ABA327DC94E69D678CAF)` 16 | 17 | We can then use `swift-pd-guess` to guess the file name using a given words array. 18 | 19 | ```shell 20 | swift-pd-guess 00D91DAB7748ABA327DC94E69D678CAF Foo,Private,API,_,+, --prefix DemoKit 21 | # Try brute force hash: 00D91DAB7748ABA327DC94E69D678CAF with dictionary ["Foo", "Private", "API", "_", "+"] max-4 22 | # Found match: DemoKitFoo+Private.swift 23 | ``` 24 | 25 | The result is `DemoKit/Foo+Private.swift` since we are lucky to find a good words array. 26 | 27 | ## Related Projects 28 | 29 | https://github.com/OpenSwiftUIProject/SwiftPrivateImportExample 30 | 31 | ## License 32 | 33 | MIT. See LICENSE for detail. 34 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - v*.*.* 7 | 8 | jobs: 9 | release: 10 | runs-on: macos-15 11 | permissions: 12 | contents: write 13 | steps: 14 | - uses: actions/checkout@v4 15 | - name: Setup Xcode 16 | uses: maxim-lobanov/setup-xcode@v1 17 | with: 18 | xcode-version: '16.1' 19 | - name: Build executable for release 20 | run: swift build -c release --arch arm64 --arch x86_64 --product swift-pd-guess 21 | - name: Compress archive 22 | run: tar -czf swift-pd-guess-${{ github.ref_name }}.tar.gz -C .build/apple/Products/Release swift-pd-guess 23 | - name: Release 24 | uses: softprops/action-gh-release@v2 25 | with: 26 | files: swift-pd-guess-${{ github.ref_name }}.tar.gz 27 | token: ${{ secrets.GITHUB_TOKEN }} 28 | - name: Compute Checksum 29 | id: checksum 30 | run: | 31 | echo "md5=$(md5 -q ./swift-pd-guess-${{ github.ref_name }}.tar.gz)" >> $GITHUB_OUTPUT 32 | echo "xcode_path=$(xcrun xcode-select --print-path)" >> $GITHUB_OUTPUT 33 | - name: Create Release 34 | id: create_release 35 | uses: ncipollo/release-action@v1 36 | with: 37 | body: | 38 | Build Xcode version ${{ steps.checksum.outputs.xcode_path }} 39 | | Name | MD5 | 40 | |----------------------------------------------|--------------------------------------------| 41 | | swift-pd-guess-${{ github.ref_name }}.tar.gz | ${{ steps.checksum.outputs.md5 }} | 42 | allowUpdates: true 43 | artifacts: swift-pd-guess-${{ github.ref_name }}.tar.gz 44 | token: ${{ secrets.GITHUB_TOKEN }} 45 | 46 | 47 | -------------------------------------------------------------------------------- /Tests/PDGuessTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PDGuessTests.swift 3 | // swift-pd-guess-tests 4 | // 5 | // Created by Kyle on 2024/12/1. 6 | // 7 | 8 | import XCTest 9 | @testable import swift_pd_guess 10 | 11 | class PDGuessTests: XCTestCase { 12 | func makeGuess(wordsCount: Int, maxCount: Int) -> PDGuess { 13 | var force = PDGuess() 14 | force.hash = "" 15 | force.words = (1...wordsCount).map(String.init).joined(separator: ",") 16 | force.prefix = "" 17 | force.suffix = "" 18 | force.maxCount = maxCount 19 | return force 20 | } 21 | 22 | // 0.022s 23 | func testMeasure8_4() { 24 | measure { 25 | let exp = expectation(description: "Finished") 26 | Task { 27 | try await makeGuess(wordsCount: 8, maxCount: 4).run() 28 | exp.fulfill() 29 | } 30 | wait(for: [exp], timeout: 200.0) 31 | } 32 | } 33 | 34 | // 1.19s 35 | func testMeasure8_8() { 36 | measure { 37 | let exp = expectation(description: "Finished") 38 | Task { 39 | try await makeGuess(wordsCount: 8, maxCount: 8).run() 40 | exp.fulfill() 41 | } 42 | wait(for: [exp], timeout: 200.0) 43 | } 44 | } 45 | 46 | // Example playground 47 | func testExample() { 48 | let exp = expectation(description: "Finished") 49 | Task { 50 | var force = PDGuess() 51 | force.hash = "3a792cb70cfcf892676d7adf8bca260f" 52 | force.words = "Color" 53 | force.prefix = "SwiftUICore" 54 | force.suffix = ".swift" 55 | force.maxCount = 5 56 | try await force.run() 57 | exp.fulfill() 58 | } 59 | wait(for: [exp], timeout: 200.0) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Sources/PDGuess.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PDGuess.swift 3 | // swift-pd-guess 4 | // 5 | // Created by Kyle on 2024/12/1. 6 | // 7 | 8 | import Foundation 9 | import ArgumentParser 10 | import Algorithms 11 | import CryptoKit 12 | 13 | @main 14 | struct PDGuess: AsyncParsableCommand { 15 | static let configuration: CommandConfiguration = CommandConfiguration(commandName: "swift-pd-guess") 16 | 17 | @Argument(help: "MD5 hash value (private-discriminator) to search for") 18 | var hash: String 19 | 20 | @Argument(help: "Possible words (comma separated)") 21 | var words: String 22 | 23 | @Option(help: "Prefix of the search (ususally the module name)") 24 | var prefix: String 25 | 26 | @Option(help: "Suffix of the search") 27 | var suffix: String = ".swift" 28 | 29 | @Option(help: "Max count of words to combine") 30 | var maxCount: Int = 4 31 | 32 | func run() async throws { 33 | let wordsArray = words.split(separator: ",").map(String.init) 34 | let targetHash = hash.uppercased() 35 | print("Try brute force hash: \(hash) with dictionary \(wordsArray) max-\(maxCount)") 36 | 37 | if let result = try await findMatch(wordsArray: wordsArray, targetHash: targetHash) { 38 | print("Found match: \(result)") 39 | } else { 40 | print("No match found") 41 | } 42 | } 43 | 44 | private func findMatch(wordsArray: [String], targetHash: String) async throws -> String? { 45 | return try await withThrowingTaskGroup(of: String?.self) { taskGroup in 46 | for length in 1...min(wordsArray.count, maxCount) { 47 | let permutations = Array(wordsArray.permutations(ofCount: length)) 48 | let chunkSize = max(permutations.count / ProcessInfo.processInfo.processorCount, 1) 49 | 50 | for chunk in permutations.chunks(ofCount: chunkSize) { 51 | let localPrefix = prefix 52 | let localSuffix = suffix 53 | 54 | taskGroup.addTask { 55 | for perm in chunk { 56 | let combined = perm.joined() 57 | let filename = "\(localPrefix)\(combined)\(localSuffix)" 58 | let computedHash = filename.md5 59 | 60 | if computedHash == targetHash { 61 | return filename 62 | } 63 | } 64 | return nil 65 | } 66 | } 67 | // Check result 68 | for try await result in taskGroup { 69 | if let match = result { 70 | taskGroup.cancelAll() 71 | return match 72 | } 73 | } 74 | } 75 | return nil 76 | } 77 | } 78 | } 79 | 80 | extension String { 81 | var md5: String { 82 | let digest = Insecure.MD5.hash(data: self.data(using: .utf8) ?? Data()) 83 | return digest.map { String(format: "%02hhX", $0) }.joined() 84 | } 85 | } 86 | --------------------------------------------------------------------------------