How to test object substitution attacks and NSSecureCoding

Marco Eidinger
3 min readMay 15, 2022

In this blog post I tell you how to test object substitution attacks and how using NSSecureCoding can prevent such attacks. I will also

  • share example code on how to use NSKeyedArchiver and NSKeyedUnArchiver either with or without NSSecureCoding.
  • explain how to archive / unarchive an object into different formats (binary vs. XML).

If you are not too familiar with the idea behind NSSecureCoding when using NSKeyedArchiver / NSKeyedUnarchiver then I recommend that you watch Apple's WWDC 2018 session Data You Can Trust.

Additionally I recommend to read the following articles as those helped me to deepen my understanding.

Object substitution attack

First let’s encode data with NSKeyedArchiver in the form of XML.

let archiver = NSKeyedArchiver(requiringSecureCoding: false) archiver.outputFormat = .xml archiver.encodeRootObject(yourObject) archiver.finishEncoding() let data = archiver.encodedData archive.write(to: localURL)

The serialization result for my custom Model class

class Model: NSObject, NSCoding {
var text: String
var amount: Double
// further code is omitted for readability
}

is the following in XML:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>$archiver</key>
<string>NSKeyedArchiver</string>
<key>$objects</key>
<array>
<string>$null</string>
<dict>
<key>$class</key>
<dict>
<key>CF$UID</key>
<integer>3</integer>
</dict>
<key>amount</key>
<real>22.219999999999999</real>
<key>text</key>
<dict>
<key>CF$UID</key>
<integer>2</integer>
</dict>
</dict>
<string>Test</string>
<dict>
<key>$classes</key>
<array>
<string>NSSecureCodingExample.Model</string>
<string>NSObject</string>
</array>
<key>$classname</key>
<string>NSSecureCodingExample.Model</string>
</dict>
</array>
<key>$top</key>
<dict>
<key>$0</key>
<dict>
<key>CF$UID</key>
<integer>1</integer>
</dict>
</dict>
<key>$version</key>
<integer>100000</integer>
</dict>
</plist>

As an attacker I can manipulate the information in this document in such a way that not Model gets loaded during deserialization but another class.

For example I can change NSSecureCodingExample.Model to NSSecureCodingExample.FakeModel which corresponds to a class in my app

import Foundation// For Testing Purposes only
class FakeModel: NSObject, NSCoding {
let text: String
let amount: Double
internal init(text: String, amount: Double) {
self.text = text
self.amount = amount
}
// ... required init?(coder: NSCoder) {
assert(1 == 2)
}
}

I can unarchive the object with NSKeyedUnarchiver later.

let unarchiver = try NSKeyedUnarchiver(forReadingFrom: data) unarchiver.requiresSecureCoding = false let decodedDataObject = try unarchiver.decodeTopLevelObject() unarchiver.finishDecoding()

The app will crash because FakeModel gets loaded during the decodeTopLevelObject execution and it the FakeModel initializer contains malicious code (an assertion).

Apple introduced the NSSecureCoding protocol and multiple API enhancements for NSKeyedArchiver and NSKeyedUnarchiver. Once you adopt those then the compiler will be able to perform gated class checks so that an incorrect initialization cannot occur.

You can find the complete code, incl. a SwiftUI test application, on GitHub.

I made an interesting finding as it appears that it is not possible to unarchive an object stored in XML format which was archived with Secure Coding. If I am mistaken then I’d love to hear from you :)

Originally published at https://blog.eidinger.info.

--

--

Marco Eidinger

Software Engineer open-source and enterprise mobile SDKs for iOS and macOS developers | @MarcoEidinger on Twitter | visit blog.eidinger.info