Skip to content

Commit

Permalink
Merge pull request #17 from rockbruno/method-names
Browse files Browse the repository at this point in the history
Method Names and more
  • Loading branch information
Bruno Rocha authored Aug 21, 2018
2 parents b54127a + d8ac5d0 commit 1a2fddf
Show file tree
Hide file tree
Showing 18 changed files with 435 additions and 420 deletions.
57 changes: 32 additions & 25 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,22 @@

SwiftShield is a tool that generates irreversible, encrypted names for your iOS project's objects (including your Pods and Storyboards) in order to protect your app from tools that reverse engineer iOS apps, like class-dump and Cycript.

```swift
class fjiovh4894bvic: XbuinvcxoDHFh3fjid {
func cxncjnx8fh83FDJSDd() {
return vPAOSNdcbif372hFKF()
}
}
```

## 🤖 Automatic mode (Swift only)

With the `-automatic` tag, SwiftShield will use SourceKit to automatically obfuscate entire projects (including dependencies). Note that the scope of SwiftShield's automatic mode is directly related to the scope of Xcode's native refactoring tool, [which doesn't refactor everything yet](https://github.com/rockbruno/swiftshield/blob/master/SOURCEKITISSUES.md). While the specific cases on the document won't be obfuscated, SwiftShield will obfuscate all Swift classes and methods that can be reverse-engineered.


## 🛡 Manual mode (Swift/OBJ-C)

Manual mode is the easiest way of running SwiftShield, but also the most time consuming. When used, SwiftShield will obfuscate properties and classes based on a tag of your choice at the end of it's name. For example, after running SwiftShield in manual mode and a tag `__s`, the following code:
If you feel like obfuscating absolutely everything - including typealiases and internal property names, you can also use Manual mode. This is the easiest way of running SwiftShield, but also the most time consuming. When used, SwiftShield will obfuscate properties and classes based on a tag of your choice at the end of it's name. For example, after running SwiftShield in manual mode and a tag `__s`, the following code:

```swift
class EncryptedVideoPlayer__s: DecryptionProtocol__s {
Expand All @@ -31,11 +43,6 @@ class fjiovh4894bvic: XbuinvcxoDHFh3fjid {
```


## 🤖 Automatic mode (Swift only, BETA)

With the `-automatic` flag, SwiftShield will use SourceKit to automatically obfuscate entire projects (including dependencies) without the need of putting tags on objects. Note that the accuracy of SwiftShield's automatic mode is directly related to the accuracy of Xcode's native refactoring tool [which unfortunately is still not perfect](https://github.com/rockbruno/swiftshield/blob/master/SOURCEKITISSUES.md), which means that some edge cases might fail to be obfuscated. Use with caution and don't expect much, but be aware that newer releases of Xcode might improve this tool's success rate.


## 💥 Dealing with encrypted crash logs / analytics

After succesfully encrypting your project, SwiftShield generates a `conversionMap.txt` file with all the changes it made to your project, allowing you to pinpoint what an encrypted object really is.
Expand Down Expand Up @@ -68,7 +75,6 @@ Manual mode:

1. Make sure your tags aren't used on things that are not supposed to be obfuscated, like hardcoded strings.


## Installation

**Warning:** SwiftShield **irreversibly overwrites** all your source files. Ideally, you should have it run only on your CI server, and on release builds.
Expand All @@ -78,49 +84,50 @@ Download the [latest release](https://github.com/rockbruno/swiftshield/releases)

## Running SwiftShield


# Manual mode
# Automatic mode

```
swiftshield -project-root /app/MyApp
swiftshield -project-root /app/MyApp -automatic-project-file /app/MyApp/MyApp.xcworkspace -automatic-project-scheme MyApp-AppStore
```
**Required Parameters:**

`-automatic`: Enables automatic mode.

`-project-root`: The root of your project. SwiftShield will use this to search for your project files, storyboards and source files.

**Optional Parameters:**
`-automatic-project-file`: Your app's main .xcodeproj/.xcworkspace file.

`-tag myTag`: Uses a custom tag. Default is `__s`.
`-automatic-project-scheme myScheme`: The main scheme to build from your `-automatic-project-file`.

**Optional Parameters:**

`-verbose`: Prints additional information.

`-show-sourcekit-queries`: Prints queries sent to SourceKit. Note that they are huge and will absolutely clutter your terminal, so use this only for bug reports and feature development!

# Automatic mode

# Manual mode

```
swiftshield -project-root /app/MyApp -automatic-project-file /app/MyApp/MyApp.xcworkspace -automatic-project-scheme MyApp-AppStore
swiftshield -project-root /app/MyApp
```
**Required Parameters:**

`-automatic`: Enables automatic mode.

`-project-root`: The root of your project. SwiftShield will use this to search for your project files, storyboards and source files.

`-automatic-project-file`: Your app's main .xcodeproj/.xcworkspace file.

`-automatic-project-scheme myScheme`: The main scheme to build from your `-automatic-project-file`.

**Optional Parameters:**

`-verbose`: Prints additional information.
`-tag myTag`: Uses a custom tag. Default is `__s`.

`-show-sourcekit-queries`: Prints queries sent to SourceKit. Note that they are huge and will absolutely clutter your terminal, so use this only for bug reports and feature development!
`-verbose`: Prints additional information.


## Next steps
## Automatic Mode Next Steps

1. Module names
2. Method names (For automatic mode)
- [X] Method names
- [ ] Properties
- [ ] Module names
- [ ] Update Extension plists (Rich Notifications / Watch main classes)


## License
Expand Down
10 changes: 5 additions & 5 deletions SOURCEKITISSUES.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Current SourceKit issues that prevent SwiftShield from working automatically
# Current known SourceKit issues that prevent SwiftShield from obfuscating *everything*

- Issues with constrained protocols (`extension Foo where RawValue: Bar`)
- Issues with explicit generics (`[MyClass]` is fine, `Swift.Array<MyClass>` isn't)
- Typealiases issues
- Rare cases where references don't get indexed
- Typealiases: Not always indexed (`typealias Foo = UIImage | extension Foo {}` - Foo is indexed as UIImage)
- Enum names: Explicit types don't get indexed (`case MyEnum.myCase` - MyEnum isn't indexed)
- Enum cases: Although they are correctly indexed, `CodingKeys` are not meant to be changed.
- Operator overloading: Operators only get indexed if they are declared in a global scope. Since most people use `public static func`, methods with names shorter than four characters don't get obfuscated.
Binary file modified bin/swiftshield
Binary file not shown.
163 changes: 117 additions & 46 deletions swiftshield-Sources/AutomaticSwiftShield.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,62 +37,93 @@ final class AutomaticSwiftShield: Protector {

extension AutomaticSwiftShield {
func index(modules: [Module]) -> ObfuscationData {
let SK = SourceKit()
let sourceKit = SourceKit()
let obfuscationData = ObfuscationData()
var fileDataArray: [(file: File, module: Module)] = []
for module in modules {
for file in module.files {
guard file.path.isEmpty == false else {
continue
}
let compilerArgs = SK.array(argv: module.compilerArguments)
Logger.log(.indexing(file: file))
let resp = SK.indexFile(filePath: file.path, compilerArgs: compilerArgs)
if let error = SK.error(resp: resp) {
Logger.log(.indexError(file: file, error: error))
exit(error: true)
fileDataArray.append((file, module))
}
}
for fileData in fileDataArray {
let file = fileData.file
let module = fileData.module
let compilerArgs = sourceKit.array(argv: module.compilerArguments)
Logger.log(.indexing(file: file))
let resp = index(sourceKit: sourceKit, file: file, args: compilerArgs)
let dict = SKApi.sourcekitd_response_get_value(resp)
sourceKit.recurseOver(childID: sourceKit.entitiesID, resp: dict) { [unowned self] dict in
guard let data = self.getNameData(from: dict,
obfuscationData: obfuscationData,
sourceKit: sourceKit) else {
return
}
let dict = SKApi.sourcekitd_response_get_value(resp)
SK.recurseOver( childID: SK.entitiesID, resp: dict) { [unowned self] dict in
let kind = dict.getUUIDString(key: SK.kindID)
guard SK.isObjectDeclaration(kind: kind),
let name = dict.getString(key: SK.nameID),
let usr = dict.getString(key: SK.usrID) else {
return
}
let obfuscatedName: String = {
guard let protected = obfuscationData.obfuscationDict[name] else {
let protected = String.random(length: self.protectedClassNameSize)
obfuscationData.obfuscationDict[name] = protected
return protected
}
return protected
}()
obfuscationData.usrDict.insert(usr)
Logger.log(.foundDeclaration(name: name, usr: usr, newName: obfuscatedName))
let name = data.name
let usr = data.usr
obfuscationData.usrDict.insert(usr)
if dict.getString(key: sourceKit.receiverID) == nil {
obfuscationData.usrRelationDict[usr] = dict
}
obfuscationData.indexedFiles.append((file,resp))
Logger.log(.foundDeclaration(name: name, usr: usr))
}
obfuscationData.indexedFiles.append((file, resp))
}
return obfuscationData
}

private func index(sourceKit: SourceKit, file: File, args: sourcekitd_object_t) -> sourcekitd_response_t {
let resp = sourceKit.indexFile(filePath: file.path, compilerArgs: args)
if let error = sourceKit.error(resp: resp) {
Logger.log(.indexError(file: file, error: error))
exit(error: true)
}
return resp
}

private func getNameData(from dict: sourcekitd_variant_t, obfuscationData: ObfuscationData, sourceKit: SourceKit) -> (name: String, usr: String, obfuscatedName: String)? {
let kind = dict.getUUIDString(key: sourceKit.kindID)
guard sourceKit.declarationType(for: kind) != nil else {
return nil
}
guard let name = dict.getString(key: sourceKit.nameID)?.trueName, let usr = dict.getString(key: sourceKit.usrID) else {
return nil
}
guard let protected = obfuscationData.obfuscationDict[name] else {
let newName = String.random(length: self.protectedClassNameSize, excluding: obfuscationData.allObfuscatedNames)
obfuscationData.obfuscationDict[name] = newName
obfuscationData.allObfuscatedNames.insert(newName)
return (name, usr, newName)
}
return (name, usr, protected)
}

func obfuscateReferences(obfuscationData: ObfuscationData) {
let SK = SourceKit()
Logger.log(.searchingReferencesOfUsr)
for (file,indexResponse) in obfuscationData.indexedFiles {
let dict = SKApi.sourcekitd_response_get_value(indexResponse)
SK.recurseOver( childID: SK.entitiesID, resp: dict, block: { dict in
SK.recurseOver(childID: SK.entitiesID, resp: dict, block: { dict in
let kind = dict.getUUIDString(key: SK.kindID)
guard SK.isObjectReference(kind: kind) else {
guard SK.referenceType(kind: kind) != nil else {
return
}
guard let usr = dict.getString(key: SK.usrID), let name = dict.getString(key: SK.nameID) else {
guard let usr = dict.getString(key: SK.usrID), let name = dict.getString(key: SK.nameID)?.trueName else {
return
}
let line = dict.getInt(key: SK.lineID)
let column = dict.getInt(key: SK.colID)
if obfuscationData.usrDict.contains(usr) {
Logger.log(.foundReference(name: name, usr: usr, at: file, line: line, column: column))
//Operators only get indexed as such if they are declared in a global scope
//Unfortunately, most people use public static func
//So we avoid obfuscating methods with small names to prevent obfuscating operators.
guard SK.referenceType(kind: kind) != .method || name.count > 4 else {
return
}
guard self.isReferencingInternalMethod(kind: kind, dict: dict, obfuscationData: obfuscationData, sourceKit: SK) == false else {
return
}
let newName = obfuscationData.obfuscationDict[name] ?? name
Logger.log(.foundReference(name: name, usr: usr, at: file, line: line, column: column, newName: newName))
let reference = ReferenceData(name: name, line: line, column: column, file: file, usr: usr)
obfuscationData.add(reference: reference, toFile: file)
}
Expand All @@ -101,31 +132,71 @@ extension AutomaticSwiftShield {
overwriteFiles(obfuscationData: obfuscationData)
}

private func isReferencingInternalMethod(kind: String, dict: sourcekitd_variant_t, obfuscationData: ObfuscationData, sourceKit: SourceKit) -> Bool {
guard sourceKit.referenceType(kind: kind) == .method else {
return false
}
guard let usr = dict.getString(key: sourceKit.usrID) else {
return false
}
if let relDict = obfuscationData.usrRelationDict[usr], relDict.data != dict.data {
return isReferencingInternalMethod(kind: kind, dict: relDict, obfuscationData: obfuscationData, sourceKit: sourceKit)
}
var isReference = false
sourceKit.recurseOver(childID: sourceKit.relatedID, resp: dict) { dict in
guard isReference == false else {
return
}
guard let usr = dict.getString(key: sourceKit.usrID) else {
return
}
if obfuscationData.usrDict.contains(usr) == false {
isReference = true
} else if let relDict = obfuscationData.usrRelationDict[usr] {
isReference = self.isReferencingInternalMethod(kind: kind, dict: relDict, obfuscationData: obfuscationData, sourceKit: sourceKit)
}
}
return isReference
}

func overwriteFiles(obfuscationData: ObfuscationData) {
for (file,references) in obfuscationData.referencesDict {
var sortedReferences = references.filterDuplicates { $0.line == $1.line && $0.column == $1.column }.sorted(by: lesserPosition)
var currentReference = 0
var line = 1
var column = 1
let data = try! String(contentsOfFile: file.path, encoding: .utf8)
var charArray = Array(data).map(String.init)
var currentCharIndex = 0
Logger.log(.overwriting(file: file))
let matches = data.match(regex: String.swiftRegex)
let obfuscatedFile = matches.flatMap { result in
let word = (data as NSString).substring(with: result.rangeAt(0))
var wordToReturn = word
if sortedReferences.isEmpty == false && line == sortedReferences[0].line && column == sortedReferences[0].column {
sortedReferences.remove(at: 0)
wordToReturn = (obfuscationData.obfuscationDict[word] ?? word)
}
if word == "\n" {
while currentCharIndex < charArray.count && currentReference < sortedReferences.count {
let reference = sortedReferences[currentReference]
if line == reference.line && column == reference.column {
let originalName = reference.name
let word = obfuscationData.obfuscationDict[originalName] ?? originalName
let wasInternalKeyword = charArray[currentCharIndex] == "`"
for i in 1..<(originalName.count + (wasInternalKeyword ? 2 : 0)) {
charArray[currentCharIndex + i] = ""
}
charArray[currentCharIndex] = word
currentReference += 1
currentCharIndex += originalName.count
column += originalName.count
if wasInternalKeyword {
charArray[currentCharIndex] = ""
}
} else if charArray[currentCharIndex] == "\n" {
line += 1
column = 1
currentCharIndex += 1
} else {
column += word.count
column += 1
currentCharIndex += 1
}
return wordToReturn
}.joined()
}
let joined = charArray.joined()
do {
try obfuscatedFile.write(toFile: file.path, atomically: false, encoding: String.Encoding.utf8)
try joined.write(toFile: file.path, atomically: false, encoding: String.Encoding.utf8)
} catch {
Logger.log(.fatal(error: error.localizedDescription))
exit(error: true)
Expand Down
52 changes: 52 additions & 0 deletions swiftshield-Sources/DynamicLink.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import Foundation

struct DynamicLinkLibrary {
let path: String
let handle: UnsafeMutableRawPointer

func load<T>(symbol: String) -> T {
if let sym = dlsym(handle, symbol) {
return unsafeBitCast(sym, to: T.self)
}
let errorString = String(validatingUTF8: dlerror()) ?? ""
fatalError("Finding symbol \(symbol) failed: \(errorString)")
}
}

func appsIn( dir: String, matcher: (_ name: String) -> Bool ) -> [String] {
return (try! FileManager.default.contentsOfDirectory(atPath: dir))
.filter( matcher ).sorted().reversed().map { "\(dir)/\($0)" }
}

#if os(Linux)
let toolchainLoader = Loader(searchPaths: [linuxSourceKitLibPath])
#else
let toolchainLoader = Loader(searchPaths: (["/Applications/Xcode.app"] +
appsIn( dir: "/Applications", matcher: { $0.hasPrefix("Xcode") } ) )
.map { $0+"/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/" } )
#endif

struct Loader {
let searchPaths: [String]

func load(path: String) -> DynamicLinkLibrary {
let fullPaths = searchPaths.map { $0.appending(path) }

// try all fullPaths that contains target file,
// then try loading with simple path that depends resolving to DYLD
for fullPath in fullPaths + [path] {
if let handle = dlopen(fullPath, RTLD_LAZY) {
return DynamicLinkLibrary(path: path, handle: handle)
}
}

fatalError("Loading \(path) from \(searchPaths)")
}
}

#if os(Linux)
private let path = "libsourcekitdInProc.so"
#else
private let path = "sourcekitd.framework/Versions/A/sourcekitd"
#endif
let library = toolchainLoader.load(path: path)
Loading

0 comments on commit 1a2fddf

Please sign in to comment.