MASTG-KNOW-0140: Source Code Integrity Checks
iOS uses code signing to verify app authenticity before launch (see Debugging). Apps can also implement additional runtime checks that inspect the Mach-O binary structure to verify the integrity of their own executable code, which complements the OS-level protection and helps detect patched or re-signed binaries. For verifying the integrity of data the app stores on the device, see Storage Integrity Checks.
A common approach is to:
- Use
dladdrto resolve the base address of the loaded binary. - Parse the 64-bit Mach-O header (
mach_header_64) and iterate through its load commands to locate the__TEXT/__textsection. - Compute a cryptographic hash over the
__textsection bytes and compare it against a stored reference value.
Older examples of this technique parse the 32-bit Mach-O structures (mach_header, segment_command, section, and LC_SEGMENT), which are not used by modern 64-bit iOS apps. The following Swift example uses the 64-bit structures (mach_header_64, segment_command_64, section_64, and LC_SEGMENT_64) and computes the hash with CC_SHA256 from CommonCrypto:
import Foundation
import MachO
import CommonCrypto
// The expected hash is computed at build time over the final __text section
// and stored (ideally obfuscated) in the binary as a reference value.
let mastgExpectedTextSectionSHA256 = "<precomputed-sha256-of-__text>"
// A dedicated anchor symbol gives `dladdr` a stable address inside this image.
@_cdecl("mastg_source_integrity_anchor")
func mastgSourceIntegrityAnchor() {}
func swiftTextSectionHash() -> String {
// Step 1: Resolve the binary base address using dladdr
var info = Dl_info()
let symbol = unsafeBitCast(
mastgSourceIntegrityAnchor as @convention(c) () -> Void,
to: UnsafeRawPointer.self
)
guard dladdr(symbol, &info) != 0, let basePtr = info.dli_fbase else {
return "Failed to resolve binary base address"
}
// Step 2: Parse the Mach-O header to locate the __TEXT/__text section
let base = UnsafeRawPointer(basePtr)
var offset = MemoryLayout<mach_header_64>.size
var codePtr: UnsafeRawPointer?
var textSize: Int = 0
let header = base.load(as: mach_header_64.self)
for _ in 0 ..< Int(header.ncmds) {
let cmd = base.load(fromByteOffset: offset, as: load_command.self)
if cmd.cmd == LC_SEGMENT_64 {
let seg = base.load(fromByteOffset: offset, as: segment_command_64.self)
let segName = withUnsafeBytes(of: seg.segname) { raw in
String(bytes: raw.prefix(while: { $0 != 0 }), encoding: .utf8) ?? ""
}
if segName == "__TEXT" {
var secOffset = offset + MemoryLayout<segment_command_64>.size
for _ in 0 ..< Int(seg.nsects) {
let sec = base.load(fromByteOffset: secOffset, as: section_64.self)
let secName = withUnsafeBytes(of: sec.sectname) { raw in
String(bytes: raw.prefix(while: { $0 != 0 }), encoding: .utf8) ?? ""
}
if secName == "__text" {
// sec.addr - seg.vmaddr is the offset of __text within the
// segment; adding it to the slid base avoids needing the ASLR slide.
let runtimeOffset = Int(sec.addr - seg.vmaddr)
codePtr = base.advanced(by: runtimeOffset)
textSize = Int(sec.size)
}
secOffset += MemoryLayout<section_64>.size
}
}
}
offset += Int(cmd.cmdsize)
}
guard textSize > 0, let codePtr = codePtr else {
return "Could not locate __TEXT/__text section"
}
// Step 3: Compute SHA-256 over the __text section and compare it to the reference value
var digest = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
CC_SHA256(codePtr, CC_LONG(textSize), &digest)
let hashHex = digest.map { String(format: "%02x", $0) }.joined()
let matchesExpectedHash = hashHex == mastgExpectedTextSectionSHA256
return """
Binary base address : \(base)
__TEXT/__text size : \(textSize) bytes
SHA-256 of __text : \(hashHex)
Expected SHA-256 : \(mastgExpectedTextSectionSHA256)
Integrity check : \(matchesExpectedHash ? "pass" : "fail")
"""
}
These checks can be bypassed on jailbroken devices, for example by patching the stored reference hash or hooking the comparison logic at runtime.