How to test object substitution attacks and NSSecureCoding

Β·

3 min read

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) {
        /**
         If serialized object in XML format was modified to use FakeModel

         <dict>
             <key>$classes</key>
             <array>
                 <string>NSSecureCodingExample.FakeModel</string>
                 <string>NSObject</string>
             </array>
             <key>$classname</key>
             <string>NSSecureCodingExample.FakeModel</string>
         </dict>

         Then app would crash :--!!!
         */
        assert(1 == 2)
    }
}

I can unarchive the object with NSKeyedUnarchiver later.

let unarchiver = try NSKeyedUnarchiver(forReadingFrom: data) // This initializer enables requiresSecureCoding by default
unarchiver.requiresSecureCoding = false
let decodedDataObject = try unarchiver.decodeTopLevelObject()
unarchiver.finishDecoding()
// then cast decodedDataObject to your expected structured type

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.

Test Application

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 :)

Did you find this article valuable?

Support Marco Eidinger by becoming a sponsor. Any amount is appreciated!