Swift names die in nm the moment I strip. ObjC selector strings do not, because they live in __TEXT,__objc_methname as C strings the runtime has to see. This lab is a self-signed LabVault binary with one Swift class, one method that stays pure Swift, and one method marked @objc so I can still find it after strip. cryptid=1 stops the lab. FairPlay unwrap, jailbreak, AMFI bypass: out of scope.
Gate the image, then dump both name spaces
1$ file LabVault
2LabVault: Mach-O 64-bit executable arm64
3
4$ otool -l LabVault | egrep 'cmd LC_ENCRYPTION|cryptid|segname __TEXT|__objc_methname|LC_CODE'
5 cmd LC_SEGMENT_64
6 segname __TEXT
7 sectname __objc_methname
8 cmd LC_ENCRYPTION_INFO_64
9 cryptid 0
10 cmd LC_CODE_SIGNATURE
cryptid 0. If it is 1, I stop.
Lab source (the only Swift I need):
1import Foundation
2
3@objc(LabVault)
4final class LabVault: NSObject {
5 /// Pure Swift. Will vanish from nm after strip. Never in methname.
6 func secretLength(_ token: String) -> Int {
7 token.count
8 }
9
10 /// Exposed to ObjC. Selector startWithToken: survives strip.
11 @objc(startWithToken:)
12 func startWithToken(_ token: String) {
13 _ = secretLength(token)
14 insecureCopy(token)
15 }
16
17 func insecureCopy(_ token: String) {
18 var buf = [CChar](repeating: 0, count: 16)
19 token.withCString { src in
20 memcpy(&buf, src, strlen(src) + 1) // lab bug
21 }
22 _scratch = buf[0]
23 }
24}
nm before strip, then swift demangle
1$ nm LabVault | grep '$s8LabVault'
20000000100003a40 T $s8LabVaultAAC12secretLengthySiSSF
30000000100003b20 T $s8LabVaultAAC14startWithTokenyySSF
40000000100003c00 t $s8LabVaultAAC14startWithTokenyySSFTo
50000000100003d80 T $s8LabVaultAAC13insecureCopyyySSF
60000000100003f00 T $s8LabVaultAACMa
70000000100003f20 T $s8LabVaultAACN
80000000100004000 S _OBJC_CLASS_$_LabVault
9 U _objc_msgSend
10 U _memcpy
11 U _strlen
$s is the Swift 5+ mangling prefix. To on the third symbol is the ObjC thunk (T = thunk, o = Objective-C): the thing objc_msgSend actually jumps to for -[LabVault startWithToken:]. Ma / N are type metadata / nominal type descriptor. _OBJC_CLASS_$_LabVault exists only because the class inherits NSObject and is @objc(LabVault).
1$ xcrun swift demangle \
2 '$s8LabVaultAAC12secretLengthySiSSF' \
3 '$s8LabVaultAAC14startWithTokenyySSF' \
4 '$s8LabVaultAAC14startWithTokenyySSFTo' \
5 '$s8LabVaultAAC13insecureCopyyySSF'
6$s8LabVaultAAC12secretLengthySiSSF ---> LabVault.LabVault.secretLength(_:) -> Swift.Int
7$s8LabVaultAAC14startWithTokenyySSF ---> LabVault.LabVault.startWithToken(_:) -> ()
8$s8LabVaultAAC14startWithTokenyySSFTo ---> thunk for @objc LabVault.LabVault.startWithToken(_:) -> ()
9$s8LabVaultAAC13insecureCopyyySSF ---> LabVault.LabVault.insecureCopy(_:) -> ()
Reading the mangling without the tool, once: $s Swift, 8LabVault module, AA class with the same name as the module, C class, then the identifier, then the type. yySSF is (String) -> () with an empty first label. ySiSSF is (String) -> Int. I still run swift demangle instead of hand-parsing when the name has substitutions (A, B, …) in the middle.
nm after strip vs __objc_methname
1$ strip LabVault -o LabVault.stripped
2$ file LabVault.stripped
3LabVault.stripped: Mach-O 64-bit executable arm64
4
5$ nm LabVault.stripped | grep '$s8LabVault'
6$ nm LabVault.stripped | grep secretLength
7$ nm LabVault.stripped | grep startWithToken
8$ nm LabVault.stripped | grep OBJC_CLASS
90000000100004000 S _OBJC_CLASS_$_LabVault
The $s… text symbols are gone. _OBJC_CLASS_$_LabVault can remain as an exported ObjC class symbol depending on strip flags; on this run it did. The selector string is not in nm at all — it is a cstring section:
1$ otool -v -s __TEXT __objc_methname LabVault.stripped | egrep 'start|secret|insecure|length'
2Contents of (__TEXT,__objc_methname) section
30000000100004a10 startWithToken:
40000000100004a21 .cxx_destruct
5$ # no secretLength:, no insecureCopy:
secretLength and insecureCopy were never @objc, so they were never in methname. Strip cannot remove startWithToken: without breaking objc_msgSend. That is the recovery rule: if I need a name after strip, it has to have crossed the ObjC boundary.
1$ class-dump LabVault.stripped | sed -n '/LabVault/,+12p'
2@interface LabVault : NSObject
3- (void)startWithToken:(id)token;
4@end
class-dump reads __objc_methname / class dumps. One method. The two pure-Swift methods are invisible here. That is not a failure of class-dump; that is the Swift ABI.
ARM64: thunk vs Swift IMP vs the lab copy
1; otool -tV LabVault (unstripped, file VA, slide 0)
2; thunk: $s8LabVaultAAC14startWithTokenyySSFTo == -[LabVault startWithToken:]
30000000100003c00 pacibsp
40000000100003c04 stp x22, x21, [sp, #-0x30]!
50000000100003c08 stp x20, x19, [sp, #0x10]
60000000100003c0c stp x29, x30, [sp, #0x20]
70000000100003c10 add x29, sp, #0x20
80000000100003c14 mov x19, x0 ; self
90000000100003c18 mov x20, x2 ; NSString * (ObjC arg)
100000000100003c1c mov x0, x20
110000000100003c20 bl 0x100005800 ; Swift bridge NSString → String
120000000100003c24 mov x0, x19
130000000100003c28 ; x1/x2 now hold the Swift String value
140000000100003c2c bl 0x100003b20 ; $s8LabVaultAAC14startWithTokenyySSF
150000000100003c30 ldp x29, x30, [sp, #0x20]
160000000100003c34 ldp x20, x19, [sp, #0x10]
170000000100003c38 ldp x22, x21, [sp], #0x30
180000000100003c3c retab
x0 = self, x1 = _cmd, x2 = NSString * at the thunk. After the bridge, the Swift IMP takes a Swift String (two registers, not an object pointer). That is why a Frida ObjC.Object(args[2]) only makes sense on the thunk, not on $s8LabVaultAAC14startWithTokenyySSF.
1; $s8LabVaultAAC13insecureCopyyySSF
20000000100003d80 pacibsp
30000000100003d84 stp x29, x30, [sp, #-0x40]!
40000000100003d88 mov x29, sp
50000000100003d8c sub sp, sp, #0x10 ; 16-byte buf
60000000100003d90 ; Swift String in x20 / x21
70000000100003d94 bl 0x100005840 ; String.withCString
8; ... callback:
90000000100003de0 add x0, x29, #0x20 ; &buf[16]
100000000100003de4 mov x1, x19 ; const char *src
110000000100003de8 mov x2, x8 ; strlen+1, unbounded
120000000100003dec bl 0x100005900 ; _memcpy
lldb: break on the selector, or on the mangled name
Unstripped:
1(lldb) breakpoint set -n '$s8LabVaultAAC14startWithTokenyySSF'
2(lldb) breakpoint set -n '-[LabVault startWithToken:]' ; hits the To thunk
Stripped, the first command fails (no symbol). The second still works if the ObjC runtime can see the class — or I break on objc_msgSend and filter:
1(lldb) process launch --stop-at-entry
2(lldb) breakpoint set -n objc_msgSend
3(lldb) command script import lldb_sel.py
4; same helper as the objc_msgSend lab: stop only when sel == 'startWithToken:'
5[msgSend] sel=startWithToken: self=0x0000000281a0c0c0
6(lldb) po $x0
7<LabVault: 0x281a0c0c0>
8(lldb) po [$x2 length]
936
10(lldb) # do not po the token
11(lldb) disassemble -s $pc -c 8
On the stripped binary I recover the thunk address from the live objc_msgSend stop (x0/x1 → class cache → IMP), not from nm.
1$ otool -v -s __TEXT __objc_methname LabVault.stripped | grep start
20000000100004a10 startWithToken:
3$ # file VA of the cstring; lldb will slide it
Sanitized reproduction
UI pastes lab_ + 'A'*32. I log length.
1(lldb) po [$x2 length]
236
3(lldb) memory read -c 8 (char *)[(NSString *)$x2 UTF8String]
40x0000000283bb4a00: 6c 61 62 5f 41 41 41 41 lab_AAAA
5# remaining 28 bytes not copied into the note
Crash on the unstripped build (symbols still in the report):
1* thread #1, queue = 'com.apple.main-thread', stop reason = EXC_BAD_ACCESS (code=2)
2 frame #0: 0x00000001890afc2c libsystem_platform.dylib`_platform_memmove + 204
3 frame #1: 0x0000000100003dec LabVault`$s8LabVaultAAC13insecureCopyyySSF + 0x6c
4 frame #2: 0x0000000100003b70 LabVault`$s8LabVaultAAC14startWithTokenyySSF + 0x50
5 frame #3: 0x0000000100003c2c LabVault`$s8LabVaultAAC14startWithTokenyySSFTo + 0x2c
Same crash on the stripped build (names gone from frames 1–2):
1 frame #1: 0x0000000100003dec LabVault` + 0x3dec
2 frame #2: 0x0000000100003b70 LabVault` + 0x3b70
3 frame #3: 0x0000000100003c2c LabVault`-[LabVault startWithToken:] + 0x2c
Frame 3 still has the ObjC name because the thunk is the IMP the runtime registered. Frames 1–2 are recovered from the dSYM with atos, or not at all if I also stripped the dSYM. That is the operational difference @objc makes in a crash report.
ASAN:
1==412==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x...
2WRITE of size 37 at ... thread T0
3 #0 memcpy
4 #1 $s8LabVaultAAC13insecureCopyyySSF LabVault.swift:24
5 #2 $s8LabVaultAAC14startWithTokenyySSF LabVault.swift:16
6Shadow bytes around the buggy address:
7 00 00 00 00[f1]f1 f1 f1 00 00[f3]f3
Repro is: 36-byte UTF-8, 16-byte Swift Array<CChar>, memcpy size 37, pc in insecureCopy, selector startWithToken: still in methname after strip. Not a decrypt, not a jailbreak.
Closing
$s… demangles while the symbol table exists. strip takes that away. @objc writes a selector into __objc_methname and a To thunk the runtime can call; class-dump and objc_msgSend breakpoints keep working. Pure Swift methods are recovered from a dSYM or not at all. cryptid=1 still ends the session before any of this.
Commands appendix
1otool -l LabVault | egrep 'cryptid|__objc_methname'
2nm LabVault | grep '$s8LabVault'
3xcrun swift demangle '$s8LabVaultAAC14startWithTokenyySSF'
4strip LabVault -o LabVault.stripped
5nm LabVault.stripped | grep '$s'
6otool -v -s __TEXT __objc_methname LabVault.stripped
7class-dump LabVault.stripped
8xcrun lldb ./LabVault