I recently spent some time browsing the wild west of patched iOS applications, usually distributed as “cracked” IPAs or injected dylibs. I wanted to see what the current landscape of tweak development and app patching looks like. Some are incredibly clever, using advanced LLVM obfuscation passes. Others are literal garbage that crash before the app even finishes launching.
Here is a breakdown of four different apps I pulled apart this week: ServerCat, VidHub, Infuse, and YTLite.
Running cracked apps on simulators
To run cracked IPAs on simulators, I used simulator-trainer, which allows me to install IPAs and other tweaks directly on iOS simulators. Recently, I also used vphone-cli.
ServerCat
I encountered a ServerCat-1.16.1-TrialMacApp.ipa floating around. When I installed and launched it on the iOS simulator, it instantly crashed.
I grabbed the crash log from the Console app (you can also use Unexpectedly) and saw this:
Thread 2 Crashed:: Dispatch queue: com.apple.coredata.cloudkit.queue
0 com.apple.cloudkit.CloudKit 0x00000001877cd7d8 0x1876fa000 + 866264
1 com.apple.cloudkit.CloudKit 0x00000001877dd388 0x1876fa000 + 930696
2 com.apple.cloudkit.CloudKit 0x00000001877dd268 0x1876fa000 + 930408
3 com.apple.CoreData 0x0000000186e943f0 -[PFCloudKitContainerProvider containerWithIdentifier:options:] + 24
4 com.apple.CoreData 0x0000000186e4d8a4 -[PFCloudKitSetupAssistant _initializeCloudKitForObservedStore:andNoteMetadataInitialization:] + 636
5 com.apple.CoreData 0x0000000186f099f4 __52-[NSCloudKitMirroringDelegate _performSetupRequest:]_block_invoke + 416
6 com.apple.CoreData 0x0000000186f08844 __92-[NSCloudKitMirroringDelegate _openTransactionWithLabel:assertionLabel:andExecuteWorkBlock:]_block_invoke + 64
7 libdispatch.dylib 0x000000018017b314 _dispatch_call_block_and_release + 24
8 libdispatch.dylib 0x000000018017cc08 _dispatch_client_callout + 16
9 libdispatch.dylib 0x0000000180184da0 _dispatch_lane_serial_drain + 976
10 libdispatch.dylib 0x0000000180185924 _dispatch_lane_invoke + 388
11 libdispatch.dylib 0x0000000180191038 _dispatch_root_queue_drain_deferred_wlh + 276
12 libdispatch.dylib 0x0000000180190694 _dispatch_workloop_worker_thread + 440
13 libsystem_pthread.dylib 0x0000000105e82b88 _pthread_wqthread + 288
14 libsystem_pthread.dylib 0x0000000105e8198c start_wqthread + 8
The CloudKit crash
The app is using NSPersistentCloudKitContainer. CoreData tries to initialize CloudKit syncing, and CloudKit crashes while creating or opening the container. This usually happens when an app’s entitlements are stripped or modified (like when unpacking and re-signing an IPA) or when the app is running in a sandboxed environment that doesn’t have access to the keychain or iCloud account, which is the case for simulators.
To bypass this roadblock, I wrote a quick Theos tweak to hook CKContainer and force its initialization methods to return nil. This stops CoreData from attempting CloudKit sync entirely.
%hook CKContainer
+ (id)defaultContainer {
return nil;
}
+ (id)containerWithIdentifier:(id) arg1 {
return nil;
}
%end
With the hook injected, the app launched perfectly on the simulator.

I wanted to know exactly what the original patcher changed to unlock the premium features. Here is the diff view of the file changes:

It was clear the main ServerCat Mach-O binary was directly patched. I decided to write a Python script using LIEF to diff the original decrypted App Store binary against the patched one. The idea is to parse both binaries, hash the instructions of every function, and flag any mismatches.
I had some issues with LIEF’s built-in get_content_from_virtual_address, so I reimplemented it myself:
import hashlib
import lief
def va_to_file_offset(binary, va):
for segment in binary.segments:
start = segment.virtual_address
end = start + segment.virtual_size
if start <= va < end:
delta = va - start
return segment.file_offset + delta
return None
def get_function_bytes(binary, raw_data, address, size):
if size <= 0:
return b""
file_offset = va_to_file_offset(binary, address)
if file_offset is None:
return b""
end = file_offset + size
if end > len(raw_data):
size = len(raw_data) - file_offset
if size <= 0:
return b""
return raw_data[file_offset : file_offset + size]
Next, I generated the hashes:
def checksum(data):
return hashlib.sha256(data).hexdigest()
def parse_functions(binary, raw_data):
funcs = sorted(binary.functions, key=lambda f: f.address)
out = {}
for i in range(len(funcs) - 1):
cur, nxt = funcs[i], funcs[i + 1]
addr, next_addr = cur.address, nxt.address
if next_addr <= addr: # sanity check
continue
size = next_addr - addr
if size > 0x10000: # sanity limit
continue
name = cur.name or f"sub_{addr:x}"
data = get_function_bytes(binary, raw_data, addr, size)
if not data:
continue
out[addr] = {
"name": name,
"size": size,
"checksum": checksum(data),
}
return out
And compared the two binaries:
src = "./src/ServerCat.app/ServerCat"
patched = "./patched/ServerCat.app/ServerCat"
fat_src = lief.MachO.parse(src)
fat_patched = lief.MachO.parse(patched)
binary_src = fat_src.at(0)
binary_patched = fat_patched.at(0)
with open(src, "rb") as f:
src_raw = f.read()
with open(patched, "rb") as f:
patched_raw = f.read()
functions_src = parse_functions(binary_src, src_raw)
functions_patched = parse_functions(binary_patched, patched_raw)
print(f"src funcs: {len(functions_src)}, patched funcs: {len(functions_patched)}")
diff_count = 0
for addr, src_data in functions_src.items():
patched_data = functions_patched.get(addr)
if not patched_data:
continue
if src_data["checksum"] != patched_data["checksum"]:
print(f"[DIFF] {src_data['name']} @ {hex(addr)} size={src_data['size']}")
diff_count += 1
print(f"Total diffs: {diff_count}")
Running the script gave me the exact function that was touched:
% python3 diff.py
src funcs: 48497, patched funcs: 48497
[DIFF] sub_b30a4 @ 0xb30a4 size=924
Total diffs: 1
Opening the patched binary in IDA at base+0xb30a4 shows a classic binary patch:
__text:00000001000B30A4|; __int64 sub_1000B30A4()
__text:00000001000B30A4|sub_1000B30A4__text:00000001000B30A4|MOV X0, #1__text:00000001000B30A8|RET__text:00000001000B30A8|; End of function sub_1000B30A4
The patcher just wiped the logic and forced it to return 1 (true). Looking at the cross-references, sub_1000B30A4 is called 20 times across the binary in licensing logic:
void __fastcall sub_10012CFDC(__int64 a1)
{
__int64 v2; // [xsp+0h] [xbp-90h] BYREF
id v3; // [xsp+8h] [xbp-88h]
id v4; // [xsp+10h] [xbp-80h]
int v5; // [xsp+1Ch] [xbp-74h]
Swift::String v6; // [xsp+20h] [xbp-70h]
id v7; // [xsp+30h] [xbp-60h]
id v8; // [xsp+38h] [xbp-58h]
__int64 v9; // [xsp+40h] [xbp-50h]
id v10; // [xsp+48h] [xbp-48h]
__int64 v11; // [xsp+50h] [xbp-40h]
unsigned __int64 v12; // [xsp+58h] [xbp-38h]
id v13; // [xsp+60h] [xbp-30h]
char *v14; // [xsp+68h] [xbp-28h]
int v15; // [xsp+74h] [xbp-1Ch]
__int64 v16; // [xsp+78h] [xbp-18h]
v11 = a1;
v16 = 0;
v12 = (*(_QWORD *)(*(_QWORD *)(type metadata accessor for Premium(0) - 8) + 64LL) + 15LL) & 0xFFFFFFFFFFFFFFF0LL;
v14 = (char *)&v2 - v12;
v16 = a1;
v13 = (id)sub_10012A9D4();
sub_1004BF324();
objc_release(v13);
v15 = sub_1000B30A4(); // now always returns 1
sub_1000B3614(v14);
if ( (v15 & 1) != 0 ) // if the license is valid
{
v10 = (id)sub_10012A9D4();
v4 = (id)sub_10012A9D4();
v8 = (id)sub_1004BBF34();
objc_release(v4);
v7 = (id)sub_1000A118C();
v5 = 1;
v6 = String.init(_builtinStringLiteral:utf8CodeUnitCount:isASCII:)("term", 4u, 1);
v9 = sub_1004EB2A4(v7, v6._countAndFlagsBits);
swift_bridgeObjectRelease(v6._object);
objc_release(v7);
objc_release(v8);
sub_1004BE98C(v9);
objc_release(v10);
}
else // if the license is invalid
{
v3 = (id)sub_10012A9D4();
sub_1004BD56C(0);
objc_release(v3);
}
}
I also noticed a string modification in the hex editor that my script missed. Because the string was edited in place, its memory address didn’t change, so the assembly referencing it remained identical.

The FairPlay DRM files
While digging through the .app bundle, I noticed an SC_Info folder. You usually see Frameworks and PlugIns, but SC_Info (Secure Content Info) is where Apple’s FairPlay DRM stores iTunes Store licensing metadata.
% ls -l SC_Info
Manifest.plist ServerCat.sinf ServerCat.supf ServerCat.supp ServerCat.supx
I read that those files are part of Apple’s FairPlay DRM with iTunes Store licensing infrastructure.
The Manifest.plist contains the following:
<?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>SinfPaths</key>
<array>
<string>SC_Info/ServerCat.sinf</string>
</array>
<key>SinfReplicationPaths</key>
<array>
<string>SC_Info/ServerCat.sinf</string>
</array>
</dict>
</plist>
ServerCat.sinf holds DRM info formatted in MPEG-4 style boxes (4-byte size + 4-char type + payload):
% xxd -c 30 ./SC_Info/ServerCat.sinf
00000000: 0000 0430 7369 6e66 0000 000c 6672 6d61 6761 6d65 0000 0014 7363 686d 0000 ...0sinf....frmagame....schm..
0000001e: 0000 6974 756e 0000 0000 0000 0380 7363 6869 0000 000c 7573 6572 cf1c da65 ..itun........schi....user...e
0000003c: 0000 000c 6372 6474 e3f4 983d 0000 000c 6173 6474 0000 0000 0000 000c 6b65 ....crdt...=....asdt........ke
0000005a: 7920 0000 000a 0000 0018 6976 6976 baac 6e5c c91d ab1e 19a7 6e96 ad98 0633 y ........iviv..n\......n....3
00000078: 0000 0060 7269 6768 7665 4944 070c 82d3 706c 6174 0000 0002 6176 6572 0101 ...`righveID....plat....aver..
00000096: 0100 7472 616e e3f4 983d 7369 6e67 0000 0000 736f 6e67 597f 8f77 746f 6f6c ..tran...=sing....songY..wtool
000000b4: 5036 3131 6d65 6469 0000 0080 6d6f 6465 0000 2000 6869 3332 0000 0004 8a34 P611medi....mode.. .hi32.....4
000000d2: 795b ffff fffe 0000 0108 6e61 6d65 5472 6961 6c4d 6163 4170 7020 626c 6163 y[........nameTrialMacApp blac
000000f0: 6b00 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 k.............................
0000010e: 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 ..............................
0000012c: 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 ..............................
0000014a: 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 ..............................
00000168: 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 ..............................
00000186: 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 ..............................
000001a4: 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 ..............................
000001c2: 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 ..............................
000001e0: 0000 01c8 7072 6976 9bbb ca42 2b45 0d98 829c d177 b7de 1023 8407 420e 13f6 ....priv...B+E.....w...#..B...
000001fe: f22c 52eb a3a7 22f8 1d7e 1f66 a3b8 6553 87b1 32ab 73eb a9f7 7879 816f 000f .,R..."..~.f..eS..2.s...xy.o..
0000021c: d5ba c78e fa2e 3b50 0257 b7c7 773b 6f96 c20c 109a 7d56 c504 8df6 ead0 0fc3 ......;P.W..w;o.....}V........
0000023a: 20a5 1230 4a24 6e18 8b87 102c fa18 b437 b101 d120 e92e 8bb0 3022 8d41 e883 ..0J$n....,...7... ....0".A..
00000258: 7d44 0bfc f379 a3e4 26d1 3df2 ee6b 3918 985e 4d02 d174 cfb2 83c9 7bc1 d1d4 }D...y..&.=..k9..^M..t....{...
00000276: 0ab2 0982 dd24 4cf4 0871 fa7b 6571 ca58 f312 1a95 b08e 28bc 9515 37d3 b7a9 .....$L..q.{eq.X......(...7...
00000294: fd82 a5bc 161e ed09 a077 3798 81d1 1451 6f20 3b9a 0150 4c6b 95e9 102a 68e8 .........w7....Qo ;..PLk...*h.
000002b2: 77b5 50ec 66e1 7837 4f21 3a6a 13cd 318e 65ab 0098 8af9 b0f1 ba84 6a91 ceec w.P.f.x7O!:j..1.e.........j...
000002d0: 990c b555 691e 33c5 b408 c21d 58d8 3f6b 75cf c2c9 ee29 4183 a332 4e6a fd2f ...Ui.3.....X.?ku....)A..2Nj./
000002ee: 5fc7 0e42 2af7 a392 71a1 2264 b94e 3fb5 51bc 0d97 2920 8726 fb9c 5fe8 d2ca _..B*...q."d.N?.Q...) .&.._...
0000030c: d0de 877c 7148 91f7 3ad2 4eec 3bc9 90d6 c066 44f8 268b 8fea 534c 2853 890c ...|qH..:.N.;....fD.&...SL(S..
0000032a: 1e1f a035 811f 1fa2 6e69 b6ba 31fa a57b f632 81c2 2482 1e3e 06e6 2852 111a ...5....ni..1..{.2..$..>..(R..
00000348: a679 fd7c 612e 1acc d77f 1abd d461 221a 2c64 e08d 43a0 7897 1918 da7b e1a9 .y.|a........a".,d..C.x....{..
00000366: 03fe 6119 d4a3 28f2 a051 628f 007f c7d6 2176 b12e 7e84 f812 4451 3bd0 c78e ..a...(..Qb.....!v..~...DQ;...
00000384: 9f00 1af3 b31c b20b 6d10 f071 2108 d8bf 1c17 09b2 df20 7240 75fe 0e04 c8c9 ........m..q!........ r@u.....
000003a2: de9f d24a 8450 0000 0088 7369 676e a111 7d1e d853 3543 805e 11d1 16f0 fa7f ...J.P....sign..}..S5C.^......
000003c0: 86b3 c9e6 845d d35a 06df 4b52 266c af3e ad52 4305 7b05 64f8 8cc3 d8de 029c .....].Z..KR&l.>.RC.{.d.......
000003de: 77f7 a444 2a38 5d12 9541 08f4 3cb0 b2c2 3c74 02e4 d048 dc20 4525 9315 8b00 w..D*8]..A..<...<t...H. E%....
000003fc: 97bb 4465 8db4 bc36 3948 7ea5 191b 5892 2a63 b668 24c4 0602 a2c5 6cea a645 ..De...69H~...X.*c.h$.....l..E
0000041a: b003 1925 c2ba ece3 9db7 a383 c8d8 d79c 420e 0f61 b006 ...%............B..a..
I passed it through a parsing script Claude quickly made and got this:
[sinf] offset=0x0000 size=1072
[frma] offset=0x0008 size=12 → original_format = 'game'
[schm] offset=0x0014 size=20 → scheme_type = 'itun', version = 0x00000000
[schi] offset=0x0028 size=896
[user] offset=0x0030 size=12 → user_id = 0xcf1cda65
[crdt] offset=0x003c size=12 → creation_date (unix) = 0xe3f4983d (3824457789)
[asdt] offset=0x0048 size=12 → asset_date (unix) = 0x00000000 (0)
[key ] offset=0x0054 size=12 → key_data = 0000000a
[iviv] offset=0x0060 size=24 → iv = baac6e5cc91dab1e19a76e96ad980633
[righ] offset=0x0078 size=96
[veID] offset=0x0080 size=8 → vendor_id = 0x070c82d3
[plat] offset=0x0088 size=8 → platform = 0x00000002 (iPhone)
[aver] offset=0x0090 size=8 → app_version = 01010100
[tran] offset=0x0098 size=8 → transaction_id = 0xe3f4983d
[sing] offset=0x00a0 size=8 → singleton_flags = 00000000
[song] offset=0x00a8 size=8 → content_id = 0x597f8f77
[tool] offset=0x00b0 size=8 → tool = 'P611'
[medi] offset=0x00b8 size=8 payload_preview=00000080...
[mode] offset=0x00c0 size=8 → drm_mode = 0x00002000
[hi32] offset=0x00c8 size=8 → key_hi32 = 0x00000004
[name] offset=0x00d8 size=264 → name = 'TrialMacApp black'
[priv] offset=0x01e0 size=456 → encrypted_blob (448 bytes) = 9bbbca422b450d98829cd177b7de1023...
[sign] offset=0x03a8 size=136 → rsa_signature (128 bytes) = a1117d1ed8533543805e11d116f0fa7f...
ServerCat.supf is a FairPlay license file, it contains the following:
% binwalk ./SC_Info/ServerCat.supf
DECIMAL HEXADECIMAL DESCRIPTION
--------------------------------------------------------------------------------
84 0x54 Certificate in DER format (x509 v3), header length: 4, sequence length: 704
Using CyberChef, dropping the first 84 bytes and parsing the rest as a raw X.509 certificate:
Drop_bytes(0,84,false)Parse_X.509_certificate('Raw')
Version: 3 (0x02)
Serial number: 4056631623655487166234866221073 (0x3333af080708af0001af000011)
Algorithm ID: SHA1withRSA
Validity
Not Before: 08/07/2008 00:48:29 (dd-mm-yyyy hh:mm:ss) (080708004829Z)
Not After: 07/07/2013 00:48:29 (dd-mm-yyyy hh:mm:ss) (130707004829Z)
Issuer
C = US
O = Apple Inc.
OU = Apple Certification Authority
CN = Apple FairPlay Certification Authority
Subject
C = US
O = Apple Inc.
OU = Apple FairPlay
CN = AP.3333AF080708AF0001AF000011
Public Key
Algorithm: RSA
Length: 1024 bits
Modulus: bf:90:13:7a:74:6e:0f:8e:95:16:83:cf:07:dd:c5:76:
e0:58:0b:3a:5f:f8:e9:51:41:86:5c:db:66:61:b9:65:
6b:d2:14:1a:f2:92:a6:17:b4:0a:be:f7:9d:2a:7d:af:
a2:67:f4:bc:9a:61:b8:9b:da:2e:c0:fc:94:2d:5d:4d:
ca:a6:39:0c:de:97:09:6a:80:a6:43:4c:da:64:ec:ba:
c7:57:43:78:ba:54:ee:4b:74:10:c1:db:f3:9a:d7:6b:
46:7e:1e:e6:aa:8b:28:d2:66:8b:e1:6d:47:49:c3:0f:
e1:63:5b:b4:b7:17:92:4a:66:24:f1:af:54:b0:5f:d5
Exponent: 65537 (0x10001)
Certificate Signature
Algorithm: SHA1withRSA
Signature: 6b:9b:8c:6c:c9:a2:83:a1:7b:23:0e:d2:e4:bd:77:34:
8a:b7:28:17:43:e9:f2:6b:51:5b:24:26:0a:d5:fb:be:
07:f7:f3:98:30:74:de:c4:66:80:b6:f1:f8:7f:76:d4:
80:e1:3a:69:67:b3:f1:e1:1d:91:c3:8e:b4:6b:60:21:
50:d4:6a:a9:86:a4:65:50:97:29:b6:9c:84:c1:19:f8:
c0:11:d0:03:db:3c:fd:36:62:87:31:4f:8d:51:d0:ac:
de:f7:7e:b8:2a:68:f1:ed:8a:4f:85:63:3d:5d:97:ed:
34:c9:54:64:73:5e:5d:a6:84:c2:fd:6a:5b:93:2b:ff
Extensions
keyUsage CRITICAL:
digitalSignature,keyEncipherment,dataEncipherment,keyAgreement
basicConstraints CRITICAL:
{}
subjectKeyIdentifier :
8e46a12cb19bb592c770dba82e2da28f7293ec26
authorityKeyIdentifier :
kid=fa0dd411911be6b24e1e06499411dd6362075964
ServerCat.supf is likely a license file on the device:
% binwalk ./SC_Info/ServerCat.supp
DECIMAL HEXADECIMAL DESCRIPTION
--------------------------------------------------------------------------------
800 0x320 Certificate in DER format (x509 v3), header length: 4, sequence length: 704
Drop_bytes(0,800,false)Parse_X.509_certificate('Raw')
Version: 3 (0x02)
Serial number: 4056631623655487166234866221058 (0x3333af080708af0001af000002)
Algorithm ID: SHA1withRSA
Validity
Not Before: 08/07/2008 00:48:26 (dd-mm-yyyy hh:mm:ss) (080708004826Z)
Not After: 07/07/2013 00:48:26 (dd-mm-yyyy hh:mm:ss) (130707004826Z)
Issuer
C = US
O = Apple Inc.
OU = Apple Certification Authority
CN = Apple FairPlay Certification Authority
Subject
C = US
O = Apple Inc.
OU = Apple FairPlay
CN = AP.3333AF080708AF0001AF000002
Public Key
Algorithm: RSA
Length: 1024 bits
Modulus: b8:21:6b:38:68:7b:41:8d:4e:ac:23:66:10:d1:ca:9c:
f4:90:e5:70:e5:1d:1c:25:12:0a:79:f9:5b:db:cc:1d:
6d:26:df:9f:2b:75:76:28:23:a0:72:f7:60:2e:07:54:
cd:78:6d:9f:4b:34:45:f3:03:e0:88:d9:94:18:97:9d:
29:b1:b3:36:df:57:8b:50:d0:e8:10:a1:4b:6f:c4:42:
d8:cf:6f:ba:11:05:b9:b4:fc:1a:13:4b:69:a0:46:aa:
19:cf:08:00:cb:d2:cc:03:cf:7c:ba:fb:5c:b0:bf:ad:
cf:14:88:eb:4d:e1:bc:5b:82:f2:00:88:97:fb:7d:2f
Exponent: 65537 (0x10001)
Certificate Signature
Algorithm: SHA1withRSA
Signature: 9e:c3:d5:4e:a2:dd:42:5e:d2:02:7b:e5:ea:49:a0:30:
87:b7:ca:b9:d0:3a:da:8f:23:7e:a2:54:ee:1b:b1:71:
8b:f7:f0:f6:f4:36:dd:67:b5:b1:ba:09:35:a5:44:a5:
b3:2a:2c:03:1d:a5:51:d3:96:7d:f6:e9:74:29:1a:0a:
d3:08:5f:46:19:9e:1f:72:22:18:92:37:e1:02:1c:03:
49:ed:47:eb:1c:89:69:39:0c:c1:eb:67:c5:eb:75:59:
8a:5e:a6:dc:2b:a4:bd:f8:ce:49:3e:b7:2a:00:9c:9b:
71:90:39:35:ea:67:e6:7f:5b:bc:2b:61:09:42:6d:02
Extensions
keyUsage CRITICAL:
digitalSignature,keyEncipherment,dataEncipherment,keyAgreement
basicConstraints CRITICAL:
{}
subjectKeyIdentifier :
0922efcefffd694d817d8c72eee936301f5fbb6e
authorityKeyIdentifier :
kid=fa0dd411911be6b24e1e06499411dd6362075964
The last one is ServerCat.supx:
00000000: 00000001 -> version/type ?
00000004: 00000040 -> length of following block = 64 bytes
00000008: 00000001 -> number of entries ?
0000000c: 00000010 -> 16 bytes
00000010: 8fd54d07cb43a478c98c6bea4e3334be -> random nonce / IV
00000020: 00000002 -> second entry type ?
00000024: 00000010 -> 16 bytes
00000028: ccfc70af2efd7bea0d9323209fcd5481
00000038: 00000000
0000003c: 00000000
00000040: ef3985c16ec7e0401866ac68aaae306a
This file contains a mysterious 16-byte random nonce/IV. Not much is documented publicly about the internal FairPlay file formats beyond standard SINF, so I left it there.
VidHub
Next, I looked at a “patched” version of VidHub. This one was entirely broken and crashed immediately on launch due to an infinite recursion bug on Thread 5:
Exception Type: EXC_BAD_ACCESS (SIGBUS)
Exception Codes: KERN_PROTECTION_FAILURE at 0x000000016bdaffd0
STACK GUARD 16bdac000-16bdb0000
thread 5 crashed
Thread 5 Crashed:: com.apple.uikit.eventfetch-thread
0 ??? 0x249756bec
1 ??? 0x249778e6c
2 ??? 0x249755e80
3 ??? 0x249778e6c
4 ??? 0x249755e80
5 ??? 0x249778e6c
...
I dumped the injected Vidhub.dylib into IDA.

It was a mess. Function names were scrambled, control flow was heavily obfuscated, there were no strings, and it imported dlsym 32 times to resolve everything dynamically at runtime.
Here is what the decompiled init function looks like:See the raw pseudo code
void init()
{
int v1; // w8
uint8x8_t v2; // d0
int i; // w8
int v8; // w9
__int64 (__fastcall *v9)(__int64, __int64); // x26
__int64 (__fastcall *v10)(int *); // x0
__int64 v11; // x0
int16x8_t v12; // q0
int16x8_t v13; // q0
int16x8_t v14; // q0
int16x8_t v15; // q0
int16x8_t v16; // q0
int16x8_t v17; // q0
__int64 (__fastcall *v18)(__int64); // x0
void (__fastcall *v19)(__int64, __int64 (__fastcall *)(id)); // x0
__int64 (__fastcall *v20)(__int64 *); // x0
__int64 v21; // x24
__int64 (__fastcall *v22)(__int64, __int64); // x26
__int64 (__fastcall *v23)(int *); // x0
__int64 v24; // x0
__int64 (__fastcall *v25)(__int64); // x0
void (__fastcall *v26)(__int64, __int64 (__fastcall *)(int, int, id)); // x0
uint8x8_t v27; // d0
int16x8_t v28; // q0
int16x8_t v29; // q0
int16x8_t v30; // q0
int16x8_t v31; // q0
__int64 (__fastcall *v32)(__CFString *, __int64); // x0
__int64 v33; // x0
__int64 v34; // x24
__int64 (__fastcall *v35)(__int64, __int64); // x0
__int64 v36; // x0
__int64 v37; // x26
__int64 (__fastcall *v38)(__int64, __int64); // x0
uint8x8_t v39; // d0
int16x8_t v40; // q0
int16x8_t v41; // q0
__int64 (__fastcall *v42)(char *); // x0
__int64 (__fastcall *v43)(__int64); // x0
char v44; // [xsp+60h] [xbp-110h]
char v45; // [xsp+64h] [xbp-10Ch]
__int64 v46; // [xsp+68h] [xbp-108h]
__int64 v47; // [xsp+70h] [xbp-100h]
__int64 v48; // [xsp+78h] [xbp-F8h]
__int64 (__fastcall *v49)(__int64); // [xsp+80h] [xbp-F0h]
__int64 v50; // [xsp+88h] [xbp-E8h]
__int64 (__fastcall *v51)(__int64); // [xsp+90h] [xbp-E0h]
__int64 v52; // [xsp+98h] [xbp-D8h]
__int64 v53; // [xsp+A0h] [xbp-D0h]
__int64 (__fastcall *v54)(__CFString *, __int64); // [xsp+A8h] [xbp-C8h]
__int64 (__fastcall *v55)(char *); // [xsp+B0h] [xbp-C0h]
int v56; // [xsp+BCh] [xbp-B4h]
__int64 v57; // [xsp+C0h] [xbp-B0h]
__int64 v58; // [xsp+C8h] [xbp-A8h]
__int64 v59; // [xsp+D0h] [xbp-A0h]
if ( atomic_load((unsigned int *)&dword_1074C) )
v1 = -66742549;
else
v1 = -779423966;
v56 = v1;
v2.i32[1] = 3932311;
for ( i = 507978117; ; i = -1824828365 )
{
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
v8 = i;
if ( i > -66742550 )
break;
if ( i <= -1120637577 )
{
if ( i > -1283432139 )
{
if ( i == -1283432138 )
{
v55 = (__int64 (__fastcall *)(char *))dlsym((void *)0xFFFFFFFFFFFFFFFELL, &byte_10690);
v58 = v55(&byte_106AC);
i = 593833484;
}
else
{
byte_10632 = v45;
byte_10633 = byte_10613 - ((2 * byte_10613) & 0xA2) + 81;
byte_10634 = byte_10614 - ((2 * byte_10614) & 0xB0) + 88;
byte_10635 = byte_10615 ^ 0xEF;
byte_10636 = ~((byte_10616 | 0x16) & (~byte_10616 | 0xE9));
v27 = (uint8x8_t)veor_s8((int8x8_t)qword_10617, (int8x8_t)0xBC6BAA2D6E361E4ELL);
qword_10637 = (__int64)v27;
v27.i32[0] = dword_1061F;
v28 = (int16x8_t)vmovl_u8(v27);
*(int8x8_t *)v28.i8 = veor_s8(*(int8x8_t *)v28.i8, (int8x8_t)0xD100CE00C60065LL);
*(int8x8_t *)v28.i8 = vmovn_s16(v28);
dword_1063F = v28.i32[0];
byte_10643 = byte_10623 - ((2 * byte_10623) & 0x92) - 55;
v28.i32[0] = dword_10624;
v29 = (int16x8_t)vmovl_u8(*(uint8x8_t *)v28.i8);
*(int8x8_t *)v29.i8 = veor_s8(*(int8x8_t *)v29.i8, (int8x8_t)0x6C00D10022001BLL);
*(int8x8_t *)v29.i8 = vmovn_s16(v29);
dword_10644 = v29.i32[0];
byte_10648 = byte_10628 ^ 0xD8;
byte_10657 = byte_10649;
byte_10658 = byte_1064A ^ 0x26;
byte_10659 = byte_1064B ^ 0xB;
byte_1065A = byte_1064C - ((2 * byte_1064C) & 0x50) - 88;
byte_1065B = byte_1064D ^ 0x36;
byte_1065C = byte_1064E ^ 0xF0;
byte_1065D = byte_1064F ^ 0xFA;
byte_1065E = byte_10650 - ((2 * byte_10650) & 0xE8) - 12;
byte_1065F = byte_10651 ^ 0x36;
byte_10660 = byte_10652 - ((2 * byte_10652) & 0xB6) - 37;
v29.i32[0] = dword_10653;
v30 = (int16x8_t)vmovl_u8(*(uint8x8_t *)v29.i8);
*(int8x8_t *)v30.i8 = veor_s8(*(int8x8_t *)v30.i8, (int8x8_t)0x16001C00C40057LL);
dword_10661 = vmovn_s16(v30).u32[0];
byte_10690 = byte_10670 ^ 0xB8;
byte_10691 = byte_10671 ^ 0x9C;
byte_10692 = byte_10672 - ((2 * byte_10672) & 0xC7) + 99;
byte_10693 = byte_10673 - ((2 * byte_10673) & 0xE7) - 13;
qword_10694 = (__int64)veor_s8((int8x8_t)qword_10674, (int8x8_t)0x5F17A81E62B070E2LL);
byte_1069C = byte_1067C ^ 0x4A;
byte_1069D = byte_1067D ^ 0x57;
byte_1069E = byte_1067E - ((2 * byte_1067E) & 0xA) + 5;
byte_1069F = byte_1067F ^ 0xF8;
byte_106A0 = ~((byte_10680 | 0xA3) & (~byte_10680 | 0x5C));
byte_106AC = byte_106A1 - ((2 * byte_106A1) & 0x6A) + 53;
byte_106AD = byte_106A2 - ((2 * byte_106A2) & 0x55) - 86;
*(int8x8_t *)v30.i8 = veor_s8((int8x8_t)qword_106A3, (int8x8_t)0xFC1FD4CA860FF479LL);
qword_106AE = v30.i64[0];
byte_106B6 = byte_106AB - ((2 * byte_106AB) & 0x9A) - 51;
v30.i32[0] = dword_106B8;
byte_106C8 = byte_106BC ^ 0xDA;
byte_106C9 = byte_106BD ^ 0x63;
v31 = (int16x8_t)vmovl_u8(*(uint8x8_t *)v30.i8);
*(int8x8_t *)v31.i8 = veor_s8(*(int8x8_t *)v31.i8, (int8x8_t)0xF3005B00930048LL);
v2 = (uint8x8_t)vmovn_s16(v31);
dword_106C4 = v2.i32[0];
byte_106CA = byte_106BE - ((2 * byte_106BE) & 0x96) - 53;
byte_106CB = byte_106BF ^ 0xB1;
byte_106CC = byte_106C0 ^ 0x51;
i = -66742549;
}
}
else if ( i == -1824828365 )
{
v43 = (__int64 (__fastcall *)(__int64))dlsym((void *)0xFFFFFFFFFFFFFFFELL, &byte_105F0);
v54 = (__int64 (__fastcall *)(__CFString *, __int64))v43(v46);
i = -923185557;
}
else if ( v59 )
{
i = -1283432138;
}
else
{
i = 522459525;
}
}
else if ( i <= -923185558 )
{
if ( i == -1120637576 )
{
tdYUYQheIIoR = v53;
i = 721729605;
}
else
{
v25 = (__int64 (__fastcall *)(__int64))dlsym((void *)0xFFFFFFFFFFFFFFFELL, &byte_105F0);
v53 = v25(v57);
i = -1120637576;
}
}
else if ( i == -923185557 )
{
v36 = v54(CFSTR("d\x8BJ\xD1\x5FVi_\xAE\xA8'\x92"), v48);
v37 = v49(v36);
v38 = (__int64 (__fastcall *)(__int64, __int64))dlsym(
(void *)0xFFFFFFFFFFFFFFFELL,
(const char *)&qword_105B0);
v57 = v38(v50, v37);
if ( v57 )
i = -1064055453;
else
i = 522459525;
}
else if ( i == -846681663 )
{
aD[0] = v44 - ((2 * v44) & 0x78) + 60;
*(int8x8_t *)&aD[1] = veor_s8((int8x8_t)qword_10570, (int8x8_t)0x2DDD3C5493C6689ELL);
aD[9] = byte_10578 ^ 0x14;
aD[10] = byte_10579 ^ 0x85;
aD[11] = byte_1057A - ((2 * byte_1057A) & 0x4E) + 39;
v39 = (uint8x8_t)veor_s8((int8x8_t)qword_10590, (int8x8_t)0x97F964DBC05B19D2LL);
qword_105B0 = (__int64)v39;
v39.i32[0] = dword_10598;
v40 = (int16x8_t)vmovl_u8(v39);
*(int8x8_t *)v40.i8 = veor_s8(*(int8x8_t *)v40.i8, (int8x8_t)0x1900B00020004ALL);
*(int8x8_t *)v40.i8 = vmovn_s16(v40);
dword_105B8 = v40.i32[0];
byte_105BC = byte_1059C ^ 0xF6;
byte_105BD = byte_1059D ^ 0xF6;
byte_105BE = byte_1059E - 2 * (byte_1059E & 3) + 3;
v40.i32[0] = dword_1059F;
v41 = (int16x8_t)vmovl_u8(*(uint8x8_t *)v40.i8);
*(int8x8_t *)v41.i8 = veor_s8(*(int8x8_t *)v41.i8, (int8x8_t)0x3C0097008A00BFLL);
v2 = (uint8x8_t)vmovn_s16(v41);
dword_105BF = v2.i32[0];
byte_105C3 = byte_105A3 ^ 0x50;
byte_105C4 = byte_105A4 - 2 * (byte_105A4 & 0xF) + 15;
byte_105C5 = byte_105A5 ^ 0x43;
byte_105C6 = byte_105A6 ^ 0xCD;
byte_105C7 = ~((byte_105A7 | 0x71) & (~byte_105A7 | 0x8E));
byte_10630 = byte_10610 ^ 0xBD;
byte_10631 = byte_10611 ^ 0x35;
v45 = byte_10612 ^ 0x5B;
i = -1170759660;
}
else
{
byte_105F0 = byte_105D0 - ((2 * byte_105D0) & 0xB6) - 37;
v2.i32[0] = dword_105D1;
v12 = (int16x8_t)vmovl_u8(v2);
*(int8x8_t *)v12.i8 = veor_s8(*(int8x8_t *)v12.i8, (int8x8_t)0x5000630088002DLL);
*(int8x8_t *)v12.i8 = vmovn_s16(v12);
dword_105F1 = v12.i32[0];
byte_105F5 = byte_105D5 ^ 0xAB;
byte_105F6 = byte_105D6 - ((2 * byte_105D6) & 0xEC) - 10;
byte_105F7 = byte_105D7 ^ 0xBC;
byte_105F8 = byte_105D8 ^ 0xA5;
byte_105F9 = byte_105D9 - ((2 * byte_105D9) & 0x9C) + 78;
v12.i32[0] = dword_105DA;
v13 = (int16x8_t)vmovl_u8(*(uint8x8_t *)v12.i8);
*(int8x8_t *)v13.i8 = veor_s8(*(int8x8_t *)v13.i8, (int8x8_t)0x250095008900BFLL);
*(int8x8_t *)v13.i8 = vmovn_s16(v13);
dword_105FA = v13.i32[0];
byte_105FE = byte_105DE - ((2 * byte_105DE) & 0x28) + 20;
v13.i32[0] = dword_105DF;
v14 = (int16x8_t)vmovl_u8(*(uint8x8_t *)v13.i8);
*(int8x8_t *)v14.i8 = veor_s8(*(int8x8_t *)v14.i8, (int8x8_t)0xCC00A700C100BBLL);
*(int8x8_t *)v14.i8 = vmovn_s16(v14);
dword_105FF = v14.i32[0];
byte_10603 = byte_105E3 ^ 0x93;
byte_10604 = byte_105E4 - ((2 * byte_105E4) & 0x55) - 86;
byte_10605 = byte_105E5 ^ 0x99;
byte_10606 = byte_105E6 ^ 0xEB;
byte_10607 = byte_105E7 - ((2 * byte_105E7) & 0x8A) + 69;
byte_10608 = byte_105E8 ^ 0xDE;
byte_10520 = byte_10500 ^ 0x74;
byte_10521 = byte_10501 ^ 0x84;
byte_10522 = byte_10502 - ((2 * byte_10502) & 0xC8) - 28;
v14.i32[0] = dword_10503;
v15 = (int16x8_t)vmovl_u8(*(uint8x8_t *)v14.i8);
*(int8x8_t *)v15.i8 = veor_s8(*(int8x8_t *)v15.i8, (int8x8_t)0x4E000A00D800BDLL);
dword_10523 = vmovn_s16(v15).u32[0];
byte_10527 = byte_10507 ^ 0x6E;
byte_10528 = byte_10508 - ((2 * byte_10508) & 0x64) - 78;
byte_10529 = byte_10509 ^ 0xC6;
byte_1052A = byte_1050A ^ 0x6A;
byte_1052B = byte_1050B - ((2 * byte_1050B) & 0x72) - 71;
byte_1052C = byte_1050C ^ 0x32;
byte_1052D = byte_1050D - ((2 * byte_1050D) & 0x9C) - 50;
*(int8x8_t *)v15.i8 = veor_s8((int8x8_t)qword_1050E, (int8x8_t)0x9A011B41E82CEA0DLL);
qword_1052E = v15.i64[0];
byte_10536 = byte_10516 ^ 0xB;
byte_10537 = byte_10517 ^ 0xDB;
v15.i32[0] = dword_10538;
v16 = (int16x8_t)vmovl_u8(*(uint8x8_t *)v15.i8);
*(int8x8_t *)v16.i8 = veor_s8(*(int8x8_t *)v16.i8, (int8x8_t)0x7200AF0075008CLL);
*(_DWORD *)asc_10544 = vmovn_s16(v16).u32[0];
asc_10544[4] = byte_1053C ^ 0xE9;
asc_10544[5] = byte_1053D ^ 0xFA;
asc_10544[6] = byte_1053E ^ 0xC1;
asc_10544[7] = byte_1053F - ((2 * byte_1053F) & 0x38) - 100;
asc_10544[8] = byte_10540 ^ 0x87;
asc_10544[9] = byte_10541 - ((2 * byte_10541) & 0x2E) - 105;
asc_10544[10] = ~((byte_10542 | 0x3F) & (~byte_10542 | 0xC0));
asc_10544[11] = byte_10543 ^ 0x19;
*(int8x8_t *)v16.i8 = veor_s8((int8x8_t)qword_10550, (int8x8_t)0xB526670B24D17C96LL);
qword_10560 = v16.i64[0];
byte_10568 = byte_10558 - ((2 * byte_10558) & 0xDA) + 109;
v16.i32[0] = dword_10559;
v17 = (int16x8_t)vmovl_u8(*(uint8x8_t *)v16.i8);
*(int8x8_t *)v17.i8 = veor_s8(*(int8x8_t *)v17.i8, (int8x8_t)0x2900FD00E300B5LL);
v2 = (uint8x8_t)vmovn_s16(v17);
dword_10569 = v2.i32[0];
byte_1056D = ~((byte_1055D | 0x6F) & (~byte_1055D | 0x90));
byte_1056E = byte_1055E ^ 0x76;
v44 = byte_1056F;
i = -846681663;
}
}
if ( i > 637355930 )
break;
if ( i <= 522459524 )
{
if ( i == -66742549 )
{
atomic_store(1u, (unsigned int *)&dword_1074C);
i = 1054072251;
}
else
{
i = 637355931;
}
}
else if ( i == 522459525 )
{
__asm { SVC 0xF813 }
i = 1948622362;
}
else if ( i == 580958046 )
{
v32 = (__int64 (__fastcall *)(__CFString *, __int64))v51(v52);
v33 = v32(CFSTR("\xE9\x08\x7B{#\xB6\x11\xC9\x6C\xC8\xF9B"), v58);
v34 = v55((char *)v33);
v35 = (__int64 (__fastcall *)(__int64, __int64))dlsym(
(void *)0xFFFFFFFFFFFFFFFELL,
(const char *)&qword_105B0);
v47 = v35(v59, v34);
if ( v47 )
i = 1549769233;
else
i = 522459525;
}
else
{
v9 = (__int64 (__fastcall *)(__int64, __int64))dlsym(
(void *)0xFFFFFFFFFFFFFFFELL,
(const char *)&qword_105B0);
v10 = (__int64 (__fastcall *)(int *))dlsym((void *)0xFFFFFFFFFFFFFFFELL, &byte_10657);
v11 = v10(&dword_106C4);
v52 = v9(v11, v58);
v51 = (__int64 (__fastcall *)(__int64))dlsym((void *)0xFFFFFFFFFFFFFFFELL, &byte_105F0);
i = 580958046;
}
}
if ( i > 1054072250 )
break;
i = v56;
if ( v8 != 637355931 )
{
v26 = (void (__fastcall *)(__int64, __int64 (__fastcall *)(int, int, id)))dlsym(
(void *)0xFFFFFFFFFFFFFFFELL,
&byte_10630);
v26(v57, hiJIHYHDFzii);
i = 522459525;
}
}
if ( i != 1054072251 )
break;
v42 = (__int64 (__fastcall *)(char *))dlsym((void *)0xFFFFFFFFFFFFFFFELL, &byte_10657);
v59 = v42(&byte_10520);
i = -1793244380;
}
if ( i != 1549769233 )
break;
v18 = (__int64 (__fastcall *)(__int64))dlsym((void *)0xFFFFFFFFFFFFFFFELL, &byte_105F0);
UEwzIxqjFpNO = (_UNKNOWN *)v18(v47);
v19 = (void (__fastcall *)(__int64, __int64 (__fastcall *)(id)))dlsym((void *)0xFFFFFFFFFFFFFFFELL, &byte_10630);
v19(v47, ElMUjXpGwLYn);
v20 = (__int64 (__fastcall *)(__int64 *))dlsym((void *)0xFFFFFFFFFFFFFFFELL, &byte_10657);
v50 = v20(&qword_10560);
v49 = (__int64 (__fastcall *)(__int64))dlsym((void *)0xFFFFFFFFFFFFFFFELL, &byte_10690);
v21 = v49((__int64)&byte_106AC);
v22 = (__int64 (__fastcall *)(__int64, __int64))dlsym((void *)0xFFFFFFFFFFFFFFFELL, (const char *)&qword_105B0);
v23 = (__int64 (__fastcall *)(int *))dlsym((void *)0xFFFFFFFFFFFFFFFELL, &byte_10657);
v24 = v23(&dword_106C4);
v48 = v21;
v46 = v22(v24, v21);
}
}
I loaded the app into LLDB, waited for the module to load, and set a breakpoint on dlsym:
(lldbinit) process attach --name MediaCenter-iOS --waitfor
(lldbinit) bm "......../Vidhub.dylib"
(lldbinit) c
Process 41671 resuming
[+] Hit module loading: ......../Vidhub.dylib @ 0x104c30000
Process 41671 stopped
* thread #1, stop reason = breakpoint 1.1
frame #0: 0x0000000104a268cc dyld`lldb_image_notifier
I checked the dlsym stub address in IDA (0xBB04) and set a breakpoint:
(lldbinit) breakpoint set -a 0x104c30000+0xBB04
Breakpoint 2: where = Vidhub.dylib`symbol stub for: dlsym, address = 0x0000000104c3bb04
On the first hit, register x1 contained the symbol string being resolved:
Process 41671 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 2.1
frame #0: 0x0000000104c3bb04 Vidhub.dylib`dlsym
(lldbinit) register read x1
x1 = 0x0000000104c40657 _dyld_private + 1575
(lldbinit) x/s $x1
0x104c40657: "objc_getClass"
Stepping through the dlsym breakpoints, it was grabbing standard Objective-C runtime functions: sel_registerName, class_getInstanceMethod, method_getImplementation, and method_setImplementation.
I stepped out (finish) to see what class was being requested via objc_getClass:
(lldbinit) finish
Process 44987 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = step out
frame #0: 0x0000000102958e20 Vidhub.dylib`init + 3616
(lldbinit) register read x0
x0 = 0x0000000180087758 libobjc.A.dylib`objc_getClass
I set a breakpoint directly on objc_getClass to catch what string was being passed in x0:
(lldbinit) breakpoint set -a 0x0000000180087758
Breakpoint 3: where = libobjc.A.dylib`objc_getClass, address = 0x0000000180087758
Process 44987 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 3.1
frame #0: 0x0000000180087758 libobjc.A.dylib`objc_getClass
(lldbinit) register read x0
x0 = 0x0000000102960520 _dyld_private + 1264
(lldbinit) x/s $x0
0x102960520: "MediaCenter_iOS.AboutVC"
Later on, I caught it failing during method replacement on class_getInstanceMethod, which ultimately resulted in the infinite loop. Because the patch itself was fundamentally broken (likely incompatible with the specific Swift/Obj-C class structure of this app version), I abandoned reversing this one.
Infuse
Infuse was next. After fixing another CloudKit crash using the exact same method as ServerCat, the app launched, presenting me with a custom “ad” screen from the patcher:

Looking at the bundle structure, there was a dedicated iPatchDylibs folder:
% ls ./Payload/infuse.app/iPatchDylibs
Infuse.dylib libblackjack.dylib libhooker.dylib libsubstrate.dylib
It looks like the dev used iPatch, pulling in standard Substrate and libhooker binaries. However, Infuse.dylib doesn’t directly import them.
Opening Infuse.dylib in IDA showed heavy LLVM obfuscation, specifically from Hikari.
Here are some projects worth checking out if you want to learn more about Hikari:
- https://github.com/HikariObfuscator/Core
- https://github.com/lich4/ollvm-pass
- https://github.com/19h/chernobog
Here is the init function of Infuse.dylib:Details
void init()
{
unsigned int v0; // w8
NSBundle *v1; // x0
CGAffineTransform *(__cdecl **v2)(CGAffineTransform *__return_ptr __struct_ptr, CGFloat, CGFloat); // x9
CGAffineTransform *(__cdecl **v3)(CGAffineTransform *__return_ptr __struct_ptr, CGFloat, CGFloat); // x8
__CFString *v4; // x8
__CFString *v5; // x0
NSUserDefaults *v6; // x0
id *v7; // x8
id v8; // x0
id *v9; // x8
__int64 v10; // x0
SEL v11; // x0
__int64 *v12; // x8
__int64 v13; // x0
const char *TypeEncoding; // x0
Class *v15; // x10
SEL *v16; // x9
const char **v17; // x8
__int64 v18; // [xsp-80h] [xbp-180h] BYREF
__int64 v19; // [xsp-70h] [xbp-170h] BYREF
__int64 v20; // [xsp-60h] [xbp-160h] BYREF
__int64 v21; // [xsp-50h] [xbp-150h] BYREF
__int64 v22; // [xsp-40h] [xbp-140h] BYREF
__int64 v23; // [xsp-30h] [xbp-130h] BYREF
__int64 v24; // [xsp-20h] [xbp-120h] BYREF
__int64 v25; // [xsp-10h] [xbp-110h] BYREF
id v26; // [xsp+8h] [xbp-F8h]
__int64 v27; // [xsp+10h] [xbp-F0h]
const char *v28; // [xsp+18h] [xbp-E8h]
id v29; // [xsp+20h] [xbp-E0h]
__CFString *v30; // [xsp+28h] [xbp-D8h]
__int64 *v31; // [xsp+30h] [xbp-D0h]
SEL *v32; // [xsp+38h] [xbp-C8h]
Method *v33; // [xsp+40h] [xbp-C0h]
__int64 *v34; // [xsp+48h] [xbp-B8h]
const char **v35; // [xsp+50h] [xbp-B0h]
CGAffineTransform *(__cdecl **v36)(CGAffineTransform *__return_ptr __struct_ptr, CGFloat, CGFloat); // [xsp+58h] [xbp-A8h]
CGAffineTransform *(__cdecl **v37)(CGAffineTransform *__return_ptr __struct_ptr, CGFloat, CGFloat); // [xsp+60h] [xbp-A0h]
id v38; // [xsp+68h] [xbp-98h]
NSUserDefaults *v39; // [xsp+70h] [xbp-90h]
id *v40; // [xsp+78h] [xbp-88h]
id *v41; // [xsp+80h] [xbp-80h]
id *v42; // [xsp+88h] [xbp-78h]
int v43; // [xsp+94h] [xbp-6Ch]
char *v44; // [xsp+98h] [xbp-68h]
char *v45; // [xsp+A0h] [xbp-60h]
char *v46; // [xsp+A8h] [xbp-58h]
char *v47; // [xsp+B0h] [xbp-50h]
int v48; // [xsp+BCh] [xbp-44h]
char *v49; // [xsp+C0h] [xbp-40h]
char *v50; // [xsp+C8h] [xbp-38h]
char *v51; // [xsp+D0h] [xbp-30h]
char *v52; // [xsp+D8h] [xbp-28h]
int v53; // [xsp+E4h] [xbp-1Ch]
_BOOL4 v54; // [xsp+E8h] [xbp-18h]
unsigned int v55; // [xsp+ECh] [xbp-14h]
v0 = atomic_load((unsigned int *)&dword_14C68);
v55 = v0;
v54 = v0 != 0;
if ( !v0 )
{
aLe[0] = 72;
aLe[1] = asc_14A40[1] - 23 - 2 * (asc_14A40[1] & 0xE9);
v46 = asc_14A40;
v47 = aLe;
aLe[2] = 65;
aLe[3] = 112;
v48 = 2;
aLe[4] = 112;
aLe[5] = 86;
aLe[6] = 101;
aLe[7] = 114;
aLe[8] = 115;
aLe[9] = 105;
aLe[10] = 111;
aLe[11] = 110;
aLe[12] = 0;
v49 = asc_14A60;
v50 = asc_14A80;
strcpy(asc_14A80, "CFBundleShortVersionString");
v51 = asc_14AA0;
v52 = aU;
qmemcpy(aU, "FCInAppPurc", 11);
aU[11] = ~(asc_14AA0[11] ^ 0xDD);
aU[12] = 97;
aU[13] = 115;
aU[14] = 101;
aU[15] = asc_14AA0[15] - 72 - 2 * (asc_14AA0[15] & 0xB8);
aU[16] = 101;
aU[17] = 114;
aU[18] = 118;
aU[19] = asc_14AA0[19] - (2 * (~(asc_14AA0[19] ^ 0xC0) & 0xC0) + 64);
aU[20] = 99;
aU[21] = 101;
aU[22] = 70;
aU[23] = asc_14AA0[23] - (2 * (~(asc_14AA0[23] ^ 0x7B) & 0x7B) - 123);
aU[24] = asc_14AA0[24] - 38 - 2 * (asc_14AA0[24] & 0xDA);
aU[25] = (asc_14AA0[25] | 0x23) & (~asc_14AA0[25] | 0xDC);
v53 = (unsigned __int8)asc_14AA0[26];
v43 = 2;
aU[26] = v53 - (2 * (~(v53 ^ 0xD2) & 0xD2) + 46);
aU[27] = 105;
aU[28] = 117;
aU[29] = 109;
aU[30] = 0;
v44 = asc_14AE0;
v45 = aS;
qmemcpy(aS, "isFeaturePurchased:t", 20);
aS[20] = ~(~asc_14AE0[20] & 0x4A | 0x94);
aS[21] = 108;
aS[22] = asc_14AE0[22] - (2 * (~(asc_14AE0[22] ^ 0x9D) & 0x9D) + 99);
aS[23] = asc_14AE0[23] ^ 0x78;
aS[24] = 97;
aS[25] = 116;
aS[26] = 101;
aS[27] = ~((~asc_14AE0[27] | 0x8C) & (asc_14AE0[27] | 0x73));
aS[28] = 0;
}
atomic_store(1u, (unsigned int *)&dword_14C68);
v40 = (id *)&v25;
v41 = (id *)&v24;
v42 = (id *)&v23;
v31 = &v22;
v32 = (SEL *)&v21;
v33 = (Method *)&v20;
v34 = &v19;
v35 = (const char **)&v18;
v36 = &CGAffineTransformMakeScale_ptr;
v37 = &CGAffineTransformMakeScale_ptr;
v1 = objc_retainAutoreleasedReturnValue(+[NSBundle mainBundle](&OBJC_CLASS___NSBundle, "mainBundle"));
v2 = v36;
v3 = v37;
*v40 = v1;
v38 = objc_retainAutoreleasedReturnValue((id)HikariFunctionWrapper_336(v2[109], v3[87]));
v4 = (__CFString *)objc_retainAutoreleasedReturnValue(
objc_msgSend(
v38,
"objectForInfoDictionaryKey:",
CFSTR("\xBA\xAFo\"N\x9BR\x00\xBDA\xFC\xA6g&\x909+\xEF\x40\x25\xAC\xD4\xC8Y;;")));
v5 = _HikariFunctionWrapper_12;
_HikariFunctionWrapper_12 = v4;
objc_release(v5);
objc_release(v38);
v39 = +[NSUserDefaults standardUserDefaults](&OBJC_CLASS___NSUserDefaults, "standardUserDefaults");
v6 = objc_retainAutoreleasedReturnValue(v39);
v7 = v41;
*v41 = v6;
v8 = objc_retainAutoreleasedReturnValue(objc_msgSend(*v7, "objectForKey:", CFSTR("lE\x9Fp&r[\xF6\xC9\x62\x73r*")));
v9 = v42;
*v42 = v8;
if ( !*v9
|| (v29 = *v42,
v30 = _HikariFunctionWrapper_12,
v28 = "isEqualToString:",
(HikariFunctionWrapper_338(v29, "isEqualToString:", _HikariFunctionWrapper_12) & 1) == 0) )
{
HikariFunctionWrapper_340();
}
v10 = HikariFunctionWrapper_342(CFSTR("ϓ\t\x84f\xAA\xDB\x4Af\xFEN\"\xB1\x9As\xD8\x45\xBCX塯5\x11\xB6\n\x82\x1F"));
*v31 = v10;
v11 = NSSelectorFromString(&cfstr_S.isa);
v12 = v31;
*v32 = v11;
v27 = *v12;
v13 = HikariFunctionWrapper_344(v27, *v32);
*v33 = (Method)v13;
if ( *v33 )
{
TypeEncoding = method_getTypeEncoding(*v33);
v15 = (Class *)v31;
v16 = v32;
v17 = v35;
*v35 = TypeEncoding;
class_replaceMethod(*v15, *v16, (IMP)_HikariFunctionWrapper_11, *v17);
*(_DWORD *)v34 = 0;
}
else
{
*(_DWORD *)v34 = 1;
}
v26 = 0;
objc_storeStrong(v42, 0);
objc_storeStrong(v41, v26);
objc_storeStrong(v40, 0);
}
I ran chernobog against it, which managed to recover a few of the obfuscated strings:
void init()
{
// ...
v0 = atomic_load((unsigned int *)&dword_14C68);
v55 = v0;
v54 = v0 != 0;
if ( !v0 )
{
aLe[0:20] = .........;
v49 = asc_14A60;
v50 = asc_14A80;
strcpy(asc_14A80, "CFBundleShortVersionString");
v51 = asc_14AA0;
v52 = aU;
qmemcpy(aU, "FCInAppPurc", 11);
aU[11:30] = .........;
v44 = asc_14AE0;
v45 = aS;
qmemcpy(aS, "isFeaturePurchased:t", 20);
aS[20:28] = .........;
}
atomic_store(1u, (unsigned int *)&dword_14C68);
v40 = (id *)&v25;
v41 = (id *)&v24;
v42 = (id *)&v23;
v31 = &v22;
v32 = (SEL *)&v21;
v33 = (Method *)&v20;
v34 = &v19;
v35 = (const char **)&v18;
v36 = &CGAffineTransformMakeScale_ptr;
v37 = &CGAffineTransformMakeScale_ptr;
v1 = objc_retainAutoreleasedReturnValue(+[NSBundle mainBundle](&OBJC_CLASS___NSBundle, "mainBundle"));
v2 = v36;
v3 = v37;
*v40 = v1;
v38 = objc_retainAutoreleasedReturnValue((id)HikariFunctionWrapper_336(v2[109], v3[87]));
v4 = (__CFString *)objc_retainAutoreleasedReturnValue(objc_msgSend(v38, "objectForInfoDictionaryKey:", CFSTR("CFBundleShortVersionString")));
v5 = _HikariFunctionWrapper_12;
_HikariFunctionWrapper_12 = v4;
objc_release(v5);
objc_release(v38);
v39 = +[NSUserDefaults standardUserDefaults](&OBJC_CLASS___NSUserDefaults, "standardUserDefaults");
v6 = objc_retainAutoreleasedReturnValue(v39);
v7 = v41;
*v41 = v6;
v8 = objc_retainAutoreleasedReturnValue(objc_msgSend(*v7, "objectForKey:", CFSTR("lE\x9Fp&r[\xF6\xC9\x62\x73r*")));
v9 = v42;
*v42 = v8;
if ( !*v9
|| (v29 = *v42,
v30 = _HikariFunctionWrapper_12,
v28 = "isEqualToString:",
(HikariFunctionWrapper_338(v29, "isEqualToString:", _HikariFunctionWrapper_12) & 1) == 0) )
{
HikariFunctionWrapper_340();
}
v10 = HikariFunctionWrapper_342(CFSTR("ase?erv?ceF????ium\x00塯5\x11\xB6\n\x82\x1F"));
*v31 = v10;
v11 = NSSelectorFromString(&cfstr_S.isa);
v12 = v31;
*v32 = v11;
v27 = *v12;
v13 = HikariFunctionWrapper_344(v27, *v32);
*v33 = (Method)v13;
if ( *v33 )
{
TypeEncoding = method_getTypeEncoding(*v33);
v15 = (Class *)v31;
v16 = v32;
v17 = v35;
*v35 = TypeEncoding;
class_replaceMethod(*v15, *v16, (IMP)_HikariFunctionWrapper_11, *v17);
*(_DWORD *)v34 = 0;
}
else
{
*(_DWORD *)v34 = 1;
}
v26 = 0;
objc_storeStrong(v42, 0);
objc_storeStrong(v41, v26);
objc_storeStrong(v40, 0);
}
Since static analysis was getting messy, I jumped back into LLDB and set breakpoints on the runtime API imports:
(lldbinit) bm /Users/noham/...../iPatchDylibs/Infuse.dylib
[+] Setting breakpoint on gdb_image_notifier located at address 0x1029868cc
[+] Added '.../Infuse.dylib' to breakpoint on module load.
(lldbinit) c
Process 57527 resuming
(lldbinit) [+] Hit module loading: .../Infuse.dylib @ 0x1033d8000
(lldbinit) breakpoint set -a 0x1033d8000+0x00000000000BB2C
Breakpoint 2: where = Infuse.dylib`symbol stub for: method_getImplementation, address = 0x00000001033e3b2c
(lldbinit) breakpoint set -a 0x1033d8000+0x00000000000BAF0
Breakpoint 3: where = Infuse.dylib`symbol stub for: class_getInstanceMethod, address = 0x00000001033e3af0
(lldbinit) breakpoint set -a 0x1033d8000+0x00000000000BAA8
Breakpoint 4: where = Infuse.dylib`symbol stub for: NSClassFromString, address = 0x00000001033e3aa8
(lldbinit) breakpoint set -a 0x1033d8000+0x00000000000BB44
Breakpoint 5: where = Infuse.dylib`symbol stub for: method_setImplementation, address = 0x00000001033e3b44
(lldbinit) breakpoint set -a 0x1033d8000+0x00000000000BAFC
Breakpoint 6: where = Infuse.dylib`symbol stub for: class_replaceMethod, address = 0x00000001033e3afc
After hitting c (continue), I printed the x0 register at each breakpoint to see the strings being passed in:
Process 57527 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 4.1
frame #0: 0x00000001033e3aa8 Infuse.dylib`NSClassFromString
(lldbinit) po $x0
FCInAppPurchaseServiceFreemium
Process 57527 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 3.1
frame #0: 0x00000001033e3af0 Infuse.dylib`class_getInstanceMethod
(lldbinit) po $x0
FCInAppPurchaseServiceFreemium
Process 57527 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 6.1
frame #0: 0x00000001033e3afc Infuse.dylib`class_replaceMethod
(lldbinit) image lookup -a 0x00000001033E20F8
Address: Infuse.dylib[0x000000000000a0f8] (Infuse.dylib.__TEXT.__text + 24824)
Summary: Infuse.dylib`_HikariFunctionWrapper_11
I had my answer. _HikariFunctionWrapper_11 is replacing the implementation of - [FCInAppPurchaseServiceFreemium isFeaturePurchased:]. The implementation of the wrapper is dead simple:
__int64 _HikariFunctionWrapper_11()
{
return 1;
}
The patch dynamically hooks the DRM check and forces it to always return YES. All features unlock perfectly. That’s it!
YTLite
YTLite is a popular YouTube tweak that removes ads, integrates SponsorBlock, and adds various extra features. There have been several forks and versions [1], [2], and [3]. The most recent project, https://github.com/dayanch96/YTLite, is closed-source and has moved to a paid model.
When launching it, I was greeted with a Patreon login prompt to access the tweaked features:

I wanted to see what the developer was hiding. I dumped the app and found two layers of obfuscation: control flow obfuscation (opaque predicates/dispatchers) and string obfuscation (XOR loops at runtime).
Control flow obfuscation
I don’t have much knowledge about control flow obfuscation, but I’ll try my best to describe it here.
Here is an example of a function with control flow obfuscation (related to Patreon login):
void DVNPatreonLogin()
{
unsigned int v0; // w9
v0 = atomic_load((unsigned int *)&dword_12241FC);
__asm { BR X9 }
}
We can see that IDA failed to decompile the function. Here is the raw assembly code of the function:
__text:000000000001F3D8|; void DVNPatreonLogin()
__text:000000000001F3D8|EXPORT _DVNPatreonLogin__text:000000000001F3D8|_DVNPatreonLogin ; CODE XREF: sub_18814C+4↓p
__text:000000000001F3D8|__text:000000000001F3D8|var_68 = -0x68__text:000000000001F3D8|var_5C = -0x5C__text:000000000001F3D8|var_58 = -0x58__text:000000000001F3D8|var_50 = -0x50__text:000000000001F3D8|var_40 = -0x40__text:000000000001F3D8|var_30 = -0x30__text:000000000001F3D8|var_20 = -0x20__text:000000000001F3D8|var_10 = -0x10__text:000000000001F3D8|var_s0 = 0__text:000000000001F3D8|__text:000000000001F3D8|SUB SP, SP, #0xA0__text:000000000001F3DC|STP X28, X27, [SP,#0x90+var_50]__text:000000000001F3E0|STP X26, X25, [SP,#0x90+var_40]__text:000000000001F3E4|__text:000000000001F3E4|loc_1F3E4 ; DATA XREF: __data:00000000011511A8↓o
__text:000000000001F3E4|; __data:00000000011514E0↓o
__text:000000000001F3E4|STP X24, X23, [SP,#0x90+var_30]__text:000000000001F3E8|STP X22, X21, [SP,#0x90+var_20]__text:000000000001F3EC|STP X20, X19, [SP,#0x90+var_10]__text:000000000001F3F0|STP X29, X30, [SP,#0x90+var_s0]__text:000000000001F3F4|ADD X29, SP, #0x90__text:000000000001F3F8|MOV X6, X0__text:000000000001F3FC|ADRL X8, ptr_loc_8686C ; -> loc_8686C
__text:000000000001F404|STR X8, [SP,#0x90+var_58]__text:000000000001F408|ADRL X8, dword_12241FC__text:000000000001F410|LDAR W9, [X8]__text:000000000001F414|CMP W9, #0__text:000000000001F418|CSET W9, EQ__text:000000000001F41C|STR W9, [SP,#0x90+var_5C]__text:000000000001F420|LDR X10, [SP,#0x90+var_58]__text:000000000001F424|LDR X9, [X10,W9,UXTW#3]__text:000000000001F428|ADRP X10, #dword_1158F70@PAGE
__text:000000000001F42C|LDR W10, [X10,#dword_1158F70@PAGEOFF]
__text:000000000001F430|MOV W11, #0x79BF7ECC__text:000000000001F438|EOR W10, W10, W11__text:000000000001F43C|NEG W10, W10__text:000000000001F440|ADD X9, X9, W10,SXTW__text:000000000001F444|ADRL X10, off_11586C0 ; -> loc_55568
__text:000000000001F44C|STR X10, [SP,#0x90+var_68]__text:000000000001F450|BR X9__text:000000000001F450|; End of function _DVNPatreonLogin
__text:000000000001F450|__text:000000000001F450|__data:0000000001158F60|ptr_loc_8686C DCQ loc_8686C ; DATA XREF: _DVNPatreonLogin+24↑o
__data:0000000001158F60|; -> loc_8686C
__data:0000000001158F68|DCQ loc_86188chernobog detected the control flow obfuscation:
[chernobog] Analysis of 1F3D8:
Detected obfuscations: 0x40010
- Indirect branches
- Indirect call obfuscation (Hikari)
Because of the indirect branching, IDA fails to decompile the function. But if we read the assembly, we can reconstruct the high-level logic:
void DVNPatreonLogin() {
int state = atomic_load(&dword_12241FC);
int index = (state == 0) ? 1 : 0;
void* table[2] = {
loc_8686C,
loc_86188
};
int key = *(int*)dword_1158F70;
int offset = -(key ^ 0x79BF7ECC);
void* target = table[index] + offset;
// prepare next stage context
next_table = off_11586C0;
goto target; // BR X9
}
It’s a state machine dispatcher. Execution hits this stub and is forwarded to the correct handler block based on the global state variable dword_12241FC. The destination offset is computed dynamically to break static analysis.
String obfuscation
Strings were hidden using hundreds of XOR assignments in the functions. Here is a snippet of how a string is constructed:
__int64 __usercall sub_1F454@<X0>(a1, ..., a21)
{
// ...
byte_1156800 = byte_11567C0 ^ 1;
byte_1156801 = byte_11567C1 ^ 0x4B;
byte_1156802 = byte_11567C2 ^ 0x34;
byte_1156803 = byte_11567C3 ^ 0xD9;
byte_1156804 = byte_11567C4 ^ 0xE1;
byte_1156805 = byte_11567C5 ^ 0xC7;
byte_1156806 = byte_11567C6 ^ 2;
byte_1156807 = byte_11567C7 ^ 0x1E;
byte_1156808 = byte_11567C8 ^ 0x1B;
byte_1156809 = byte_11567C9 ^ 0xF7;
byte_115680A = byte_11567CA ^ 0x34;
byte_115680B = byte_11567CB ^ 0x4F;
byte_115680C = byte_11567CC ^ 0xE9;
byte_115680D = byte_11567CD ^ 0xBE;
byte_115680E = byte_11567CE ^ 0xDC;
byte_115680F = byte_11567CF ^ 0x51;
byte_1156810 = byte_11567D0 ^ 0x39;
byte_1156811 = byte_11567D1 ^ 0x8B;
byte_1156812 = byte_11567D2 ^ 0xF7;
byte_1156813 = byte_11567D3 ^ 0x9C;
byte_1156814 = byte_11567D4 ^ 0x71;
byte_1156815 = byte_11567D5 ^ 0x74;
byte_1156816 = byte_11567D6 ^ 0x6E;
byte_1156817 = byte_11567D7 ^ 0x50;
byte_1156818 = byte_11567D8 ^ 0xC0;
byte_1156819 = byte_11567D9 ^ 0x82;
byte_115681A = byte_11567DA ^ 0x58;
byte_115681B = byte_11567DB ^ 0xD;
byte_115681C = byte_11567DC ^ 0x35;
byte_115681D = byte_11567DD ^ 0xDE;
byte_115681E = byte_11567DE ^ 0x35;
byte_115681F = byte_11567DF ^ 0xD0;
byte_1156820 = byte_11567E0 ^ 0xDB;
byte_1156821 = byte_11567E1 ^ 0x48;
byte_1156822 = byte_11567E2 ^ 0xB7;
byte_1156823 = byte_11567E3 ^ 0xF6;
byte_1156824 = byte_11567E4 ^ 0xBE;
byte_1156825 = byte_11567E5 ^ 0x90;
byte_1156826 = byte_11567E6 ^ 0xE0;
byte_1156827 = byte_11567E7 ^ 0xC6;
byte_1156828 = byte_11567E8 ^ 0x86;
byte_1156829 = byte_11567E9 ^ 0x97;
byte_115682A = byte_11567EA ^ 0x38;
byte_115682B = byte_11567EB ^ 0xA3;
byte_115682C = byte_11567EC ^ 0xAF;
byte_115682D = byte_11567ED ^ 0xBC;
byte_115682E = byte_11567EE ^ 0xAA;
byte_115682F = byte_11567EF ^ 0x12;
byte_1156830 = byte_11567F0 ^ 0x8F;
byte_1156831 = byte_11567F1 ^ 0x7C;
byte_1156832 = byte_11567F2 ^ 0x99;
byte_1156833 = byte_11567F3 ^ 0x62;
byte_1156834 = byte_11567F4 ^ 0xA0;
byte_1156835 = byte_11567F5 ^ 0xC9;
byte_1156836 = byte_11567F6 ^ 0x5A;
byte_1156837 = byte_11567F7 ^ 0xBD;
byte_1156838 = byte_11567F8 ^ 0x18;
byte_1156839 = byte_11567F9 ^ 0x6F;
byte_115683A = byte_11567FA ^ 0x11;
byte_115683B = byte_11567FB ^ 0xF5;
byte_1156860 = byte_1156840 ^ 0x8C;
byte_1156861 = byte_1156841;
byte_1156862 = byte_1156842 ^ 0x1A;
byte_1156863 = byte_1156843 ^ 0x3D;
byte_1156864 = byte_1156844 ^ 0x8F;
byte_1156865 = byte_1156845 ^ 0x3C;
byte_1156866 = byte_1156846 ^ 0xCD;
byte_1156867 = byte_1156847 ^ 0x70;
byte_1156868 = byte_1156848 ^ 0x46;
byte_1156869 = byte_1156849 ^ 0xD;
byte_115686A = byte_115684A ^ 0x54;
byte_115686B = byte_115684B ^ 0x4C;
byte_115686C = byte_115684C ^ 5;
byte_115686D = byte_115684D ^ 0x38;
byte_115686E = byte_115684E ^ 0xAF;
byte_115686F = byte_115684F ^ 0xAE;
byte_1156870 = byte_1156850 ^ 0x62;
byte_11567BA = byte_11567B4 ^ 0x4C;
byte_11567BB = byte_11567B5 ^ 0x75;
byte_11567BC = byte_11567B6 ^ 0xE6;
byte_11567BD = byte_11567B7 ^ 8;
byte_11567BE = byte_11567B8 ^ 0xD0;
byte_11567BF = byte_11567B9 ^ 0x81;
byte_1156790 = byte_1156760 ^ 0xBA;
byte_1156791 = byte_1156761 ^ 0xF7;
byte_1156792 = byte_1156762 ^ 0x40;
byte_1156793 = byte_1156763 ^ 0x23;
byte_1156794 = byte_1156764 ^ 0xF;
byte_1156795 = byte_1156765 ^ 0xCE;
byte_1156796 = byte_1156766 ^ 0x97;
byte_1156797 = byte_1156767 ^ 0x54;
byte_1156798 = byte_1156768 ^ 0x32;
byte_1156799 = byte_1156769 ^ 0xC9;
byte_115679A = byte_115676A ^ 0x3A;
byte_115679B = byte_115676B ^ 0x23;
byte_115679C = byte_115676C ^ 0x6F;
byte_115679D = byte_115676D ^ 0xF4;
byte_115679E = byte_115676E ^ 0x99;
byte_115679F = byte_115676F ^ 0x82;
byte_11567A0 = byte_1156770 ^ 0x2E;
byte_11567A1 = byte_1156771 ^ 0xDB;
byte_11567A2 = byte_1156772 ^ 0x2E;
byte_11567A3 = byte_1156773 ^ 0xB4;
byte_11567A4 = byte_1156774 ^ 0x74;
byte_11567A5 = byte_1156775 ^ 0x88;
byte_11567A6 = byte_1156776 ^ 0x4A;
byte_11567A7 = byte_1156777 ^ 0xD8;
byte_11567A8 = byte_1156778 ^ 0x74;
byte_11567A9 = byte_1156779 ^ 0xA5;
byte_11567AA = byte_115677A ^ 0x20;
byte_11567AB = byte_115677B ^ 0xF4;
byte_11567AC = byte_115677C ^ 0xE4;
byte_11567AD = byte_115677D ^ 0x5F;
byte_11567AE = byte_115677E ^ 0xA;
byte_11567AF = byte_115677F ^ 0xD3;
byte_11567B0 = byte_1156780 ^ 0xF4;
byte_11567B1 = byte_1156781 ^ 0x6C;
byte_11567B2 = byte_1156782 ^ 0xAF;
byte_11567B3 = byte_1156783 ^ 0xA8;
atomic_store(1u, a2);
v43 = objc_retainAutoreleasedReturnValue((id)((__int64 (__fastcall *)(id))sub_2CF70)(objc_retain(a1)));
v41 = objc_retainAutoreleasedReturnValue((id)sub_2CF90());
v21 = objc_retainAutoreleasedReturnValue((id)((__int64 (__fastcall *)(void *, const char *))sub_2CFB0)(&OBJC_CLASS___UIDevice, "currentDevice"));
v40 = objc_retainAutoreleasedReturnValue((id)sub_2CFD0());
objc_release(v21);
v42 = objc_retainAutoreleasedReturnValue((id)((__int64 (__fastcall *)(void *, const char *, char *))sub_2CFF0)(&OBJC_CLASS___NSString, "stringWithUTF8String:", &byte_1156860));
v22 = objc_retainAutoreleasedReturnValue((id)((__int64 (__fastcall *)(void *, const char *))sub_2D010)(&OBJC_CLASS___NSCharacterSet, "URLQueryAllowedCharacterSet"));
v23 = objc_retainAutoreleasedReturnValue((id)((__int64 (__fastcall *)(id, const char *, id))sub_2D030)(v43, "stringByAddingPercentEncodingWithAllowedCharacters:", v22));
v24 = objc_retainAutoreleasedReturnValue((id)((__int64 (__fastcall *)(void *, const char *))sub_2D050)(&OBJC_CLASS___NSCharacterSet, "URLQueryAllowedCharacterSet"));
v25 = objc_retainAutoreleasedReturnValue((id)((__int64 (__fastcall *)(id, const char *, id))sub_2D070)(v42, "stringByAddingPercentEncodingWithAllowedCharacters:", v24));
v26 = objc_retainAutoreleasedReturnValue((id)((__int64 (__fastcall *)(void *, const char *))sub_2D090)(&OBJC_CLASS___NSCharacterSet, "URLQueryAllowedCharacterSet"));
v27 = objc_retainAutoreleasedReturnValue((id)((__int64 (__fastcall *)(id, const char *, id))sub_2D0B0)(v41, "stringByAddingPercentEncodingWithAllowedCharacters:", v26));
v28 = objc_retainAutoreleasedReturnValue((id)((__int64 (__fastcall *)(void *, const char *))sub_2D0D0)(&OBJC_CLASS___NSCharacterSet, "URLQueryAllowedCharacterSet"));
v29 = objc_retainAutoreleasedReturnValue((id)((__int64 (__fastcall *)(id, const char *, id))sub_2D0F0)(v40, "stringByAddingPercentEncodingWithAllowedCharacters:", v28));
v30 = objc_retainAutoreleasedReturnValue((id)((__int64 (__fastcall *)(void *, const char *, _UNKNOWN **, char *, id, id, id, id))sub_2D124)(&OBJC_CLASS___NSString, "stringWithFormat:", &off_11568E0, &byte_1156790, v23, v25, v27, v29));
objc_release(v29);
objc_release(v28);
objc_release(v27);
objc_release(v26);
objc_release(v25);
objc_release(v24);
objc_release(v23);
objc_release(v22);
v31 = objc_retainAutoreleasedReturnValue((id)((__int64 (__fastcall *)(void *, const char *, id))sub_2D144)(&OBJC_CLASS___NSURL, "URLWithString:", v30));
v32 = ((__int64 (__fastcall *)(void *))sub_2D164)(&OBJC_CLASS___ASWebAuthenticationSession);
v33 = ((__int64 (__fastcall *)(__int64, const char *, id, _UNKNOWN **, Block_layout *))sub_2D184)(v32, "initWithURL:callbackURLScheme:completionHandler:", v31, &off_11568A0, &stru_10614E0);
v34 = (void *)qword_1224188;
qword_1224188 = v33;
objc_release(v34);
((void (__fastcall *)(__int64, const char *, _QWORD))sub_2D1A4)(qword_1224188, "setPrefersEphemeralWebBrowserSession:", 0);
((void (__fastcall *)(__int64, const char *, id))sub_2D1C4)(qword_1224188, "setPresentationContextProvider:", a1);
objc_release(a1);
((void (__fastcall *)(__int64, const char *))sub_2D1E8)(qword_1224188, "start");
objc_release(v31);
objc_release(v30);
objc_release(v42);
objc_release(v40);
objc_release(v41);
objc_release(v43);
__asm { SVC 0x162 }
return 31;
}
This constructs a URL and starts an OAuth flow via ASWebAuthenticationSession to validate the Patreon login.
It constructs a URL with specific parameters, launches a Safari browser window to handle the login, and sets up a callback to handle the result.
Using chernobog we can some of the decrypted strings:
strcpy(&byte_1156860, "com.?????.ytplus");
byte_11567BA = 104;
byte_11567BB = 116;
byte_11567BC = 116;
byte_11567BD = 112;
byte_11567BE = 115;
byte_11567BF = 0;
strcpy(&byte_1156790, "https://?????.com/api/patreon/login");
atomic_store(1u, a2);
(The domain is redacted since I don’t want to leak it, the dev has already taken enough flak from the Reddit community..)
But not all of them. To decrypt the rest, let’s build a deobfuscator. Instead of relying on the unpredictable assembly code, we can use the pseudocode generated by IDA to decrypt the strings of a given function. We can clearly identify a repeating decryption pattern:
byte_DEST = byte_SRC ^ 0xKEY
Here, byte_SRC is the obfuscated byte, byte_DEST is the decrypted byte, and 0xKEY is the XOR key used for decryption. We can use a regex pattern to match this structure and extract the source, destination, and key for each byte:
PATTERN_ASSIGN = re.compile(r"byte_([0-9A-Fa-f]+)\s*=\s*byte_([0-9A-Fa-f]+)\s*\^\s*(0x[0-9A-Fa-f]+|\d+)\s*;?")
Once the script identifies the individual byte operations, it needs to reconstruct the full strings. Since a 10-character string translates to 10 separate lines of pseudocode, the script must group these sequential operations into a single buffer. We can do this by tracking the destination addresses and grouping bytes written to consecutive memory locations.
With the operations grouped, the script performs the actual decryption by reading the raw encrypted bytes directly from the IDA database and applying the XOR keys extracted from the pseudocode.
Finally, to seamlessly integrate the decrypted strings back into the reverse engineering workflow, the script uses IDA’s API to insert comments directly in the decompiler view—rather than just printing them to the console.
You can find the gist for my deobfuscator here: https://gist.github.com/NohamR/39af791e6a82bab627ba1d72b37ac8d0. Running it automatically annotates the pseudocode in IDA like this:
// XOR Decrypted -> "%s?device_id=%@&bundle_id=%@&device_model=%@&ios_version=%@"
byte_1156800 = byte_11567C0 ^ 1; // XOR Decrypted -> "%s?device_id=%@&bundle_id=%@&device_model=%@&ios_version=%@"
byte_1156801 = byte_11567C1 ^ 0x4B;
...
// XOR Decrypted -> "com.?????.ytplus"
byte_1156862 = byte_1156842 ^ 0x1A; // XOR Decrypted -> "com.?????.ytplus"
...
// XOR Decrypted -> "https://?????.com/api/patreon/login"
byte_1156790 = byte_1156760 ^ 0xBA; // XOR Decrypted -> "https://?????.com/api/patreon/login"
I modified the script to run across the entire binary and dump the output to JSON. Two hours later (IDA is slow), it handed me 3,923 strings!!
Certificate pinning
Among the decrypted strings, five stood out related to the developer’s licensing backend:
https://?????.com/api/patreon/login
https://?????.com/api/patreon/fetch-token
https://?????.com/patreon/devices
https://?????.com/api/verify
https://?????.com/patreon/specialthanks
I also decrypted an interesting SHA256 base64 string inside the validation routine. Here is what the reconstructed C++ function for the device check looks like:
void __usercall sub_20864(unsigned int *a1)
{
const char *formatString = "%s?device_id=%@";
const char *baseUrl = "https://?????.com/api/patreon/fetch-token";
const char *strSuccess = "success";
const char *strJwt = "jwt";
const char *strTicket = "ticket";
const char *strTicketExpiry = "ticketExpiry";
const char *strSha256 = "sha256//ybZIDsY13KxwXU/e0VHQDVwf+iXRrShFKsRDBAqX3Eo=";
const char *strOEquals = "o=";
const char *strDeviceId = "deviceId";
const char *strToken = "token";
const char *strFloatFormat = "%.0f";
// Set atomic flag (indicates operation start)
std::atomic_store(a1, 1u);
void *context = *(void **)((__int64)a1 + 32);
// Retain the object (likely an NSString containing the raw device ID)
id deviceIdObj = (id)objc_retain(context);
id charSet = (id)objc_retainAutoreleasedReturnValue(objc_msgSend(&OBJC_CLASS___NSCharacterSet, "URLQueryAllowedCharacterSet"));
// URL Encode the device ID string
id encodedId = (id)objc_retainAutoreleasedReturnValue(
objc_msgSend(deviceIdObj, "stringByAddingPercentEncodingWithAllowedCharacters:", charSet)
);
objc_release(deviceIdObj);
id nsFormatString = (id)objc_msgSend(&OBJC_CLASS___NSString, "stringWithUTF8String:", formatString);
id nsBaseUrl = (id)objc_msgSend(&OBJC_CLASS___NSString, "stringWithUTF8String:", baseUrl);
// Construct the final Request URL
// Format: "https://?????.com/api/patreon/fetch-token?device_id=<ENCODED_ID>"
id requestUrl = (id)objc_retainAutoreleasedReturnValue(
objc_msgSend(
&OBJC_CLASS___NSString,
"stringWithFormat:",
nsFormatString, // Arg 1: "%s?device_id=%@"
nsBaseUrl, // Arg 2: Base URL
encodedId // Arg 3: Encoded Device ID
)
);
objc_release(encodedId);
objc_release(charSet);
objc_release(nsFormatString);
objc_release(nsBaseUrl);
// Perform a validation or availability check
// Note: strSha256 is not passed here in the visible assembly, but would likely
// be used in a subsequent validation step involving the requestUrl.
BOOL checkResult = (objc_msgSend((id)sub_2D554) == 0);
// Store the result on the stack
*(_DWORD *)((__int64)a1 - 124) = checkResult;
// Control flow obfuscation
__asm { BR X8 }
}
This function makes a POST request to https://?????.com/patreon/devices?token=%@¤t_device=%@&bundle_id=%@ which return a JSON response indicating whether the current device is registered to the user’s Patreon account or not.
But what about sha256//ybZIDsY13KxwXU/e0VHQDVwf+iXRrShFKsRDBAqX3Eo=?
I ran an OpenSSL query against the ?????.com server:
% openssl s_client -connect ?????.com:443 -servername ?????.com </dev/null 2>/dev/null \
| openssl x509 -pubkey -noout \
| openssl pkey -pubin -outform DER \
| openssl dgst -sha256 -binary \
| base64
ybZIDsY13KxwXU/e0VHQDVwf+iXRrShFKsRDBAqX3Eo=
(This command retrieves the SSL certificate from ?????.com, isolates the public key from the certificate, calculates the SHA-256 hash of that public key and encodes the result in Base64 format)
Yep. The app is using strict certificate pinning. It calculates the SHA-256 hash of the server’s public key and ensures it matches before completing the request. This prevents other patchers from easily spinning up a MITM server and feeding the app fake “success” responses.
Not a bad move, honestly.
I guess it’s time for us to switch to an open source YouTube tweak alternative, I found YouMod which is actively maintained and has a similar feature set to YTLite.
That’s it :)