文章目录
- 免责声明
- 授权机制
- 代码签名
- 验证废除
- 指纹生成
- 凭证植入
- 网络钩子
- 惊喜彩蛋
免责声明
本文所有内容仅用于学术研究、技术探讨及教育学习的目的,旨在帮助读者加深对软件安全及逆向工程的理解。文中描述的技术与方法并非鼓励或支持任何未经授权的行为,亦不应将本文内容用于任何未经授权的目的。软件破解及相关技术的使用需遵守各国法律法规,任何个人或机构在实践时应对自身行为负责,作者不承担因不当使用本文内容所引发的任何责任。
授权机制
启动应用,订阅弹窗映入眼帘。点击「Enter License Key」,我们进入解锁界面,任意输入一个无效的许可Key点击激活,应用弹出「This license does not exist」。
Alright,那就返回上一步,点击「Start Trial」输入邮箱地址来申请试用,很快就会收到一封包含试用许可Key的邮件,Key值如下。
RVL-HSQXA-AJASK-BLGER-PICEG-VAXKW
再次来到解锁界面输入此Key。这一次,打开抓包工具,观察网络请求与响应。先是客户端应用发起的请求中包含了刚才的许可Key和一个设备指纹。
POST /api/v1/application/license/verify HTTP/1.1
{
"key":"RVL-HSQXA-AJASK-BLGER-PICEG-VAXKW",
"fingerprint":"U0hBMjU2IGRpZ2VzdDogMjliMjUzNWNjMGM1ZTFlNzczYWEwNWFkMDQ1ZmI3MDNmZmJjNmU0OWU2ZWQwYTY2MWUxYzZhZGU3NDMwNzRiMw=="
}
随即服务器端返回了200状态码,且返回的响应正文中有个名为access_token的关键字段。仔细观察这个由点分隔的三段式结构,没错,正是JWT的典型特征。
HTTP/1.1 200 OK
{
"refresh_token": "e8991018349asc3c12dd4812d2345650fdfdaad585ed0cdc1eb9010397684ec6",
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE3NTgxODY4NjEuMDYwNjQ5OSwiZXhwIjoxNzU4MTg3MTYxLjA2MDY1LCJlbWFpbCI6InN2bm55NzdAZ21haWwuY29tIiwibGljZW5zZV9wYXRoIjoiXC9saWNlbnNlc1wvMjREQjBCM0ItNDhCQi0xNEE2LTkxQ0QtMDI4OTA4NDkyQ0UzIiwiY2xpZW50X2V4cGlyZXNfYXQiOjE3NTg0NDYzNjEuMDYwNjUsImxpY2Vuc2Vfc3R5bGUiOjAsIm9yZ2FuaXphdGlvbl9zdHlsZSI6MCwiaWRlbnRpZmllciI6IjRpR0R4bFBjaE1kMmsrK3lzOWVpTzE4eFJOYUdyV0xGWjRBeWRCaUJyOT0iLCJpc19zdWJzY3JpcHRpb25fdXNhYmxlIjpmYWxzZSwic3ViIjoiRDcxOEU1RDctOUE2OC0xNEY0LUNFRDEtNENFMkUyNEQwNTkwIiwib3JnYW5pemF0aW9uX25hbWUiOiJzdm5ueTc3QGdtYWlsLmNvbSIsImZpbmdlcnByaW50IjoiVTBoQk1qVTJJR1JwWjJWemREb2dNamxpTWpVek5XTmpNR00xWlRGbE56Y3pZV0V3TldGa01EUTFabUkzTURObVptSmpObVUwT1dVMlpXUXdZVFkyTVdVeFl6WmhaR1UzTkRNd056UmlNdz09IiwibGljZW5zZV9leHBpcnkiOjE3NTkzOTY0MDkuMDExOTU3MiwiaXNzIjoiUmV2ZWFsIGJ5IE1hbnRlbCJ9.OPReNgGoxsJora0jvmLlq37rbOqNmQiHNtgFLwFZ0RBD5wiLz6xkXpPHWlk6Ot5Y8wisld_-F6muEwEeqPyPcGKWNuD_Ot4bnG8uYx4rUx-CeiW33saDDl-mMX1GCF50917wWJH00zYzfZtZMIZMwupsUutUswDm9S6DFwUyArvXNBagUc9Rvi6-X1yBm2BHBLvOkp8p726mMQphk8MJ1GcxiwbuAIcq3Wg2FxZronT234XG2gWOAzYkubKF4GbTP6kjlpt3jSGoEk0qNKwsa6mK7WNicIVARy4-6_FzGjPvzNx9O9qsHPy8LE-fad6YaG1JmikA-XQw9QsdITttjK",
"token_type": "bearer",
"expires_in": 300
}
JWT三段内容分别是Header、Payload和Signature,我们先Base64解码第一部分,得到算法声明——RS256,一种基于RSA非对称加密的签名算法,这将意味着会涉及到一对公私钥。
{
"alg": "RS256",
"typ": "JWT"
}
继续解第二部分,Payload中包含了所有与授权相关的关键信息,如许可类型(license_style)、许可过期时间(license_expiry)、设备指纹(fingerprint),以及一个决定性的标志位——is_subscription_usable,介于是试用许可Key,当前值为false。
{
"iat": 1758186861.0606499,
"exp": 1758187161.06065,
"email": "svnny77@gmail.com",
"license_path": "/licenses/24DB0B3B-48BB-14A6-91CD-028908492CE3",
"client_expires_at": 1758446361.06065,
"license_style": 0,
"organization_style": 0,
"identifier": "4iGDxlPchMd2k++ys9eiO18xRNaGrWLFZ4AydBiBr9=",
"is_subscription_usable": false,
"sub": "D718E5D7-9A68-14F4-CED1-4CE2E24D0590",
"organization_name": "svnny77@gmail.com",
"fingerprint": "U0hBMjU2IGRpZ2VzdDogMjliMjUzNWNjMGM1ZTFlNzczYWEwNWFkMDQ1ZmI3MDNmZmJjNmU0OWU2ZWQwYTY2MWUxYzZhZGU3NDMwNzRiMw==",
"license_expiry": 1759396409.0119572,
"iss": "Reveal by Mantel"
}
至此,Reveal应用的授权机制已然清晰明了,完整流程如下。
- 客户端上报:当用户输入许可Key后,应用会附上一个设备指纹,一同发送至服务端。
- 服务端签发:服务端验证Key的有效性,并将授权信息与客户端上报的设备指纹打包成一个JWT Payload。接着,动用其私钥对整个Payload进行RS256签名,生成access_token返回至客户端。
- 客户端验证:客户端收到access_token后,将使用一份内置于应用之中的公钥来验证其签名。只有签名验证通过,才可表明这份授权凭证的确由官方签发且未被篡改。
而我们在没有私钥的情况下,就无法伪造出能通过客户端公钥验证的合法签名。这意味着,任何对Payload内容的篡改,如将is_subscription_usable改为true或是延长license_expiry,都会导致在客户端验证签名时失败,从而导致应用无法激活。
代码签名
既然伪造服务端凭证的破解方式已被非对称加密所封锁,那便只能在客户端做手脚。下面计划通过动态调试与分析,定位客户端签名校验的核心逻辑。在此之前,先对应用重签名,为后续LLDB调试做准备。
$ sudo codesign -f -s - --all-architectures --deep Reveal.app
Reveal.app: replacing existing signature
然而,应用却打不开。正好,不妨直接转入LLDB中,运行触发报错以捕获完整调用栈。
$ lldb Reveal.app
(lldb) target create "Reveal.app"
Current executable set to 'Reveal.app' (arm64).
(lldb) r
Process 21916 launched: 'Reveal.app/Contents/MacOS/Reveal' (arm64)
Reveal/ApplicationBuildTimestamp.swift:54: Fatal error: 'try!' expression unexpectedly raised an error: Reveal.(unknown context at $100e03154).ApplicationBuildTimestampError.noTimestamp
Process 21916 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = Fatal error: 'try!' expression unexpectedly raised an error: Reveal.(unknown context at $100e03154).ApplicationBuildTimestampError.noTimestamp
frame #0: 0x00000001b185796c libswiftCore.dylib`_swift_runtime_on_report
libswiftCore.dylib`:
-> 0x1b185796c <+0>: ret
libswiftCore.dylib`:
0x1b1857970 <+0>: b 0x1b185796c ; _swift_runtime_on_report
libswiftCore.dylib`:
0x1b1857974 <+0>: adrp x8, 301844
0x1b1857978 <+4>: ldrb w0, [x8, #0x6fc]
Target 0: (Reveal) stopped.
(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = Fatal error: 'try!' expression unexpectedly raised an error: Reveal.(unknown context at $100e03154).ApplicationBuildTimestampError.noTimestamp
* frame #0: 0x00000001b185796c libswiftCore.dylib`_swift_runtime_on_report
frame #1: 0x00000001b19009f8 libswiftCore.dylib`_swift_stdlib_reportFatalErrorInFile + 208
frame #2: 0x00000001b14f6330 libswiftCore.dylib`closure #1 (Swift.UnsafeBufferPointer<Swift.UInt8>) -> () in closure #1 (Swift.UnsafeBufferPointer<Swift.UInt8>) -> () in Swift._assertionFailure(_: Swift.StaticString, _: Swift.String, file: Swift.StaticString, line: Swift.UInt, flags: Swift.UInt32) -> Swift.Never + 104
frame #3: 0x00000001b14f5428 libswiftCore.dylib`Swift._assertionFailure(_: Swift.StaticString, _: Swift.String, file: Swift.StaticString, line: Swift.UInt, flags: Swift.UInt32) -> Swift.Never + 260
frame #4: 0x00000001b155c390 libswiftCore.dylib`swift_unexpectedError + 496
frame #5: 0x000000010012d454 Reveal`___lldb_unnamed_symbol12335 + 1204
frame #6: 0x000000010003f604 Reveal`___lldb_unnamed_symbol6634 + 196
frame #7: 0x000000010003f920 Reveal`___lldb_unnamed_symbol6635 + 112
....
根据以上信息,转至frame #5 0x10012d454。由于调用栈中显示的地址是BL指令执行后的返回点,存在+4的偏移,所以实际的函数调用位于此地址之上的0x10012d454-4,其处于sub_10012CFA0中的loc_10012D438分支。
__text:0x110012D438 loc_10012D438 ; CODE XREF: sub_10012CFA0+444↑j
__text:0x110012D438 ; sub_10012CFA0+454↑j ...
__text:0x110012D438 ADRL X1, aRevealApplicat_0 ; "Reveal/ApplicationBuildTimestamp.swift"
__text:0x110012D440 MOV X0, X19
__text:0x110012D444 MOV W2, #0x26 ; '&'
__text:0x110012D448 MOV W3, #1
__text:0x110012D44C MOV W4, #0x36 ; '6'
__text:0x110012D450 BL _swift_unexpectedError
__text:0x110012D454 BRK #1
下面是相应汇编片段的伪码。
if ( (*(unsigned int (__fastcall **)(char *, __int64, __int64))(v5 + 48))(v3, 1LL, v4) == 1 )
{
v47 = sub_10012D49C(v3);
v48 = sub_10012D4DC(v47);
v46 = swift_allocError(&type metadata for ApplicationBuildTimestampError, v48, 0LL, 0LL);
swift_willThrow();
goto LABEL_12;
}
LABEL_12:
result = swift_unexpectedError(v46, "Reveal/ApplicationBuildTimestamp.swift", 38LL, 1LL, 54LL);
__break(1u);
return result;
if代码框中对应的是分支loc_10012D408。
__text:0x10012D408 loc_10012D408 ; CODE XREF: sub_10012CFA0+1D4↑j
__text:0x10012D408 MOV X0, X28
__text:0x10012D40C BL sub_10012D49C
__text:0x10012D410 BL sub_10012D4DC
__text:0x10012D414 MOV X1, X0
__text:0x10012D418 ADRL X0, $s6Reveal30ApplicationBuildTimestampErrorON ; type metadata for ApplicationBuildTimestampError
__text:0x10012D420 MOV X2, #0
__text:0x10012D424 MOV W3, #0
__text:0x10012D428 BL _swift_allocError
__text:0x10012D42C MOV X19, X0
__text:0x10012D430 MOV X21, X0
__text:0x10012D434 BL _swift_willThrow
此分支在地址sub_10012CFA0+1D4有被引用,以下是引用处的汇编片段。开头调用了sub_100208AA4和sub_1002080A0,它们内部又有对_SecCodeCopySigningInformation与_kSecCodeInfoTimestamp的调用,这两函数分别负责提取应用的代码签名信息和签名时间戳。因而可得,该分支判断背后实际建立在签名校验结果的基础之上。
__text:0x10012D12C BL sub_100208AA4
__text:0x10012D130 CBNZ X21, loc_10012D3F8
__text:0x10012D134 MOV X25, X0
__text:0x10012D138 STUR X21, [X29,#var_80]
__text:0x10012D13C STUR X23, [X29,#var_60]
__text:0x10012D140 MOV X0, X20 ; id
__text:0x10012D144 BL _objc_release
__text:0x10012D148 MOV X8, X28
__text:0x10012D14C MOV X0, X25
__text:0x10012D150 BL sub_1002080A0
__text:0x10012D154 MOV X0, X25
__text:0x10012D158 BL _swift_bridgeObjectRelease
__text:0x10012D15C LDR X8, [X26,#0x30]
__text:0x10012D160 MOV X0, X28
__text:0x10012D164 MOV W1, #1
__text:0x10012D168 MOV X2, X19
__text:0x10012D16C BLR X8
__text:0x10012D170 CMP W0, #1
__text:0x10012D174 B.EQ loc_10012D408
之后在地址0x10012D170,程序将寄存器W0的值与1进行比较,随即分支判断随之生效,若W0的值等于1,则跳转至loc_10012D408。从该路径继续执行至loc_10012D438,最终触发ApplicationBuildTimestampError.noTimestamp错误。
因此,要规避该异常,只需将原本与1的比较改为与0进行比较,即可阻断报错分支。
__text:0x10012D170 CMP W0, #0
验证废除
成功绕过启动时的代码签名校验后,应用现已能够正常运行。下面正式分析JWT签名验证机制,并尝试将其彻底废除。
根据开头的分析,服务器端对返回的JWT Token采用了RS256签名算法。而对于Apple应用,执行此类非对称加密签名验证的标准常用途径,就是调用Security框架中的SecKeyVerifySignature函数。所以,SecKeyVerifySignature自然成为本次验证废除的关键。
__stubs:0x100C4AECC ; Boolean __cdecl SecKeyVerifySignature(SecKeyRef key, SecKeyAlgorithm algorithm, CFDataRef signedData, CFDataRef signature, CFErrorRef *error)
__stubs:0x100C4AECC _SecKeyVerifySignature ; CODE XREF: sub_1009521EC+50↑p
__stubs:0x100C4AECC ; sub_1009524D4+4C↑p
__stubs:0x100C4AECC ADRP X16, #_SecKeyVerifySignature_ptr@PAGE
__stubs:0x100C4AED0 LDR X16, [X16,#_SecKeyVerifySignature_ptr@PAGEOFF]
__stubs:0x100C4AED4 BR X16 ; __imp__SecKeyVerifySignature
为精准定位哪些地址上调用了它,我们直接在LLDB中对SecKeyVerifySignature设置断点,然后运行应用。
(lldb) br s -n SecKeyVerifySignature
Breakpoint 1: where = Security`SecKeyVerifySignature, address = 0x00000001a502e15c
(lldb) r
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
frame #0: 0x00000001a502e15c Security`SecKeyVerifySignature
Security`SecKeyVerifySignature:
-> 0x1a502e15c <+0>: pacibsp
Target 0: (Reveal) stopped.
执行finish命令至调用SecKeyVerifySignature的返回点。
(lldb) finish
* thread #1, queue = 'com.apple.main-thread', stop reason = step out
frame #0: 0x00000001002369b4 Reveal`___lldb_unnamed_symbol18939 + 80
Reveal`___lldb_unnamed_symbol18939:
-> 0x1002369b4 <+80>: mov x20, x0
Target 0: (Reveal) stopped.
查看相应调用栈。
(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = step out
* frame #0: 0x00000001002369b4 Reveal`___lldb_unnamed_symbol18939 + 80
frame #1: 0x00000001002340fc Reveal`___lldb_unnamed_symbol18827 + 216
frame #2: 0x000000010025c0b0 Reveal`___lldb_unnamed_symbol19999 + 376
frame #3: 0x000000010025a1f8 Reveal`___lldb_unnamed_symbol19980 + 244
frame #4: 0x000000010025060c Reveal`___lldb_unnamed_symbol19496 + 764
frame #5: 0x000000010024c718 Reveal`___lldb_unnamed_symbol19457 + 1248
frame #6: 0x000000010012d2c4 Reveal`___lldb_unnamed_symbol12335 + 804
frame #7: 0x000000010003f604 Reveal`___lldb_unnamed_symbol6634 + 196
frame #8: 0x000000010003f920 Reveal`___lldb_unnamed_symbol6635 + 112
....
通过dis展示反汇编指令,可确定0x1002369b4-4地址正是调用SecKeyVerifySignature之处。
(lldb) dis
Reveal`___lldb_unnamed_symbol18939:
; ...
0x1002369b0 <+76>: bl 0x100c6a60c ; symbol stub for: SecKeyVerifySignature
-> 0x1002369b4 <+80>: mov x20, x0
0x1002369b8 <+84>: mov x0, x23
0x1002369bc <+88>: bl 0x100c6b0b0 ; symbol stub for: objc_release
0x1002369c0 <+92>: mov x0, x19
0x1002369c4 <+96>: bl 0x100c6b0b0 ; symbol stub for: objc_release
0x1002369c8 <+100>: cmp w20, #0x0
0x1002369cc <+104>: cset w0, ne
0x1002369d0 <+108>: ldp x29, x30, [sp, #0x30]
0x1002369d4 <+112>: ldp x20, x19, [sp, #0x20]
0x1002369d8 <+116>: ldp x22, x21, [sp, #0x10]
0x1002369dc <+120>: ldp x24, x23, [sp], #0x40
0x1002369e0 <+124>: ret
于是我们再在地址0x1002369b0处设置断点,重启应用观察传入SecKeyVerifySignature的实参值。
(lldb) br s -a 0x1002369b0
Breakpoint 2: where = Reveal`___lldb_unnamed_symbol18939 + 76, address = 0x00000001002369b0
(lldb) r
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 2.1
frame #0: 0x00000001002369b0 Reveal`___lldb_unnamed_symbol18939 + 76
Reveal`___lldb_unnamed_symbol18939:
-> 0x1002369b0 <+76>: bl 0x100c6a60c ; symbol stub for: SecKeyVerifySignature
Target 0: (Reveal) stopped.
待中断触发,打印出传入实参值,结果如下。
- x0:2048位RSA公钥。
- x1:RS256签名算法。
- x2:JWT Header.Payload部分。
- x3:JWT Signature部分。
(lldb) register read x0 x1 x2 x3
x0 = 0x0000600000c20900
x1 = 0x00000001ff6d3030 @"algid:sign:RSA:message-PKCS1v15:SHA256"
x2 = 0x0000600000c14ed0
x3 = 0x0000600000c15c20
(lldb) expr -l objc -- @import CoreFoundation;
(lldb) expr -l objc -- @import Security;
(lldb) expr -l objc -O -- CFBridgingRelease(SecKeyCopyAttributes((SecKeyRef)$x0))
{
esiz = 2048;
priv = 0;
...
}
(lldb) expr -l objc -O -- [(NSData *)CFBridgingRelease(SecKeyCopyExternalRepresentation((SecKeyRef)$x0, NULL)) base64EncodedStringWithOptions:0]
MIIBCgKCAQEAnD9x77cVGbT+9+4odDK5+vqH4Z0JMJXPZ3eATw+aPPHQKj5GhHTJ2pUBUoazri0IIZWFYy8O3UfNF9awDmBc+4kfz1qBTpmYWGhKHKKDGGadIAGiUQxVu81lrqRqPbDUQFl7oryQIAoC7RosL2XlUM6pfNvUwMAlX6Eiuc0N/pba3cBbTdFNSPO+BG6hs7yVSH4q6DAHCMUxOzYE0YFS/f7XcY2bGWbWiKwvpqzNbuYX+JvAALBrS0/Ggr7FX/45c3aIiDlFrDEeFMlDwupjZqViUyU9GiPxa92j6VV1y2FJ+yepdKXVDdL7/zIft2TbskVilRBJ27sC2S6k2amdiwIDAQAB
(lldb) po (NSString *)$x1
algid:sign:RSA:message-PKCS1v15:SHA256
(lldb) expr -l objc -O -- [[NSString alloc] initWithData:(NSData *)$x2 encoding:4]
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE3NTgxODY4NjEuMDYwNjQ5OSwiZXhwIjoxNzU4MTg3MTYxLjA2MDY1LCJlbWFpbCI6InN2bm55NzdAZ21haWwuY29tIiwibGljZW5zZV9wYXRoIjoiXC9saWNlbnNlc1wvMjREQjBCM0ItNDhCQi0xNEE2LTkxQ0QtMDI4OTA4NDkyQ0UzIiwiY2xpZW50X2V4cGlyZXNfYXQiOjE3NTg0NDYzNjEuMDYwNjUsImxpY2Vuc2Vfc3R5bGUiOjAsIm9yZ2FuaXphdGlvbl9zdHlsZSI6MCwiaWRlbnRpZmllciI6IjRpR0R4bFBjaE1kMmsrK3lzOWVpTzE4eFJOYUdyV0xGWjRBeWRCaUJyOT0iLCJpc19zdWJzY3JpcHRpb25fdXNhYmxlIjpmYWxzZSwic3ViIjoiRDcxOEU1RDctOUE2OC0xNEY0LUNFRDEtNENFMkUyNEQwNTkwIiwib3JnYW5pemF0aW9uX25hbWUiOiJzdm5ueTc3QGdtYWlsLmNvbSIsImZpbmdlcnByaW50IjoiVTBoQk1qVTJJR1JwWjJWemREb2dNamxpTWpVek5XTmpNR00xWlRGbE56Y3pZV0V3TldGa01EUTFabUkzTURObVptSmpObVUwT1dVMlpXUXdZVFkyTVdVeFl6WmhaR1UzTkRNd056UmlNdz09IiwibGljZW5zZV9leHBpcnkiOjE3NTkzOTY0MDkuMDExOTU3MiwiaXNzIjoiUmV2ZWFsIGJ5IE1hbnRlbCJ9
(lldb) expr -l objc -O -- [(NSData *)$x3 base64EncodedStringWithOptions:0]
OPReNgGoxsJora0jvmLlq37rbOqNmQiHNtgFLwFZ0RBD5wiLz6xkXpPHWlk6Ot5Y8wisld_-F6muEwEeqPyPcGKWNuD_Ot4bnG8uYx4rUx-CeiW33saDDl-mMX1GCF50917wWJH00zYzfZtZMIZMwupsUutUswDm9S6DFwUyArvXNBagUc9Rvi6-X1yBm2BHBLvOkp8p726mMQphk8MJ1GcxiwbuAIcq3Wg2FxZronT234XG2gWOAzYkubKF4GbTP6kjlpt3jSGoEk0qNKwsa6mK7WNicIVARy4-6_FzGjPvzNx9O9qsHPy8LE-fad6YaG1JmikA-XQw9QsdITttjK
ni单步执行,进入SecKeyVerifySignature后finish至调用返回地址0x1002369b4,此时查看SecKeyVerifySignature的返回值。
-> 0x1002369b0 <+76>: bl 0x100c6a60c ; symbol stub for: SecKeyVerifySignature
(lldb) ni
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
frame #0: 0x00000001a502e15c Security`SecKeyVerifySignature
Security`SecKeyVerifySignature:
-> 0x1a502e15c <+0>: pacibsp
(lldb) finish
* thread #1, queue = 'com.apple.main-thread', stop reason = instruction step over
frame #0: 0x00000001002369b4 Reveal`___lldb_unnamed_symbol18939 + 80
Reveal`___lldb_unnamed_symbol18939:
-> 0x1002369b4 <+80>: mov x20, x0
(lldb) register read x0
x0 = 0x0000000000000001
因为并未篡改JWT Payload数据,SecKeyVerifySignature自然是返回1以表签名验证成功。
__text:0x1002369B0 BL _SecKeyVerifySignature
__text:0x1002369B4 MOV X20, X0
; ...
__text:0x1002369C8 CMP W20, #0
__text:0x1002369CC CSET W0, NE
而基于后续逻辑,若要废除JWT签名验证机制,可操作的手法并不局限于一种。
-> 0x1002369b4 <+80>: mov x20, x0
(lldb) register read w20 x20
w20 = 0xff6d3030
x20 = 0x00000001ff6d3030 @"algid:sign:RSA:message-PKCS1v15:SHA256"
(lldb) ni
-> 0x1002369b8 <+84>: mov x0, x23
(lldb) register read w20 x20
w20 = 0x00000000
x20 = 0x0000000000000000
既可直接在MOV赋值之时,改为恒赋1;也可在CMP比较之时,改为与1比较;还可在最后CSET条件设置之时,将NE改为EQ。以下三选一即可。
__text:0x1002369B4 MOV X20, #1
__text:0x1002369C8 CMP W20, #1
__text:0x1002369CC CSET W0, EQ
Patch完后,别忘了对应用重签名。
指纹生成
虽然JWT签名验证已被废除,但在授权信息中还有一个与设备唯一绑定的fingerprint指纹字段,若设备指纹与本机不符,应用仍会拒绝授权。因此,我们还需还原出指纹的生成算法。
首先,从请求流量中抓取到fingerprint字段值,注意这只是本机值,即便抓到也不能用于其他设备。对该值Base64解码,发现它并非随机字节,而是可读文本,固定前缀为“SHA256 digest: ”,后面紧随一段64字符的十六进制串,明显像是一个32字节的SHA256摘要的十六进制表示。
$ echo -n 'U0hBMjU2IGRpZ2VzdDogMjliMjUzNWNjMGM1ZTFlNzczYWEwNWFkMDQ1ZmI3MDNmZmJjNmU0OWU2ZWQwYTY2MWUxYzZhZGU3NDMwNzRiMw==' | base64 -d
SHA256 digest: 29b2535cc0c5e1e773aa05ad045fb703ffbc6e49e6ed0a661e1c6ade743074b3
由此,初步推断fingerprint是由:输入数据 → SHA256 → 十六进制 → 拼接前缀 → Base64编码,最终构成。而在IDA Pro Imports表中也看到了CryptoKit框架,并引入了与流式哈希相关接口。
CryptoKit.framework
Versions
A
CryptoKit
0x101414A10 dispatch thunk of HashFunction.init()
0x101414A00 dispatch thunk of HashFunction.update(bufferPointer:)
0x101414A08 dispatch thunk of HashFunction.finalize()
那么,为验证如上推断,我们在LLDB中采取下面两条追踪策略。
- 对base64EncodedString设置断点,以定位fingerprint的最终生成点并回溯堆栈。
- 对HashFunction.update与HashFunction.finalize设置断点,捕获被哈希的数据与最终摘要。
按照以上第一点,先对base64EncodedString设置断点,重启应用。
(lldb) br s -n base64EncodedString
Breakpoint 3: where = Foundation`Foundation.Data.base64EncodedString(options: __C.NSDataBase64EncodingOptions) -> Swift.String, address = 0x00000001a3ba3958
(lldb) r
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 3.1
frame #0: 0x00000001a3ba3958 Foundation`Foundation.Data.base64EncodedString(options: __C.NSDataBase64EncodingOptions) -> Swift.String
Foundation`Foundation.Data.base64EncodedString(options: __C.NSDataBase64EncodingOptions) -> Swift.String:
-> 0x1a3ba3958 <+0>: pacibsp
Target 0: (Reveal) stopped.
随后中断在base64EncodedString入口地址,finish至调用该函数的返回地址0x100257730。
(lldb) finish
Process 22085 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = step out
frame #0: 0x0000000100257730 Reveal`___lldb_unnamed_symbol19843 + 892
Reveal`___lldb_unnamed_symbol19843:
-> 0x100257730 <+892>: mov x19, x0
Target 0: (Reveal) stopped
不妨将base64EncodedString的返回值打印出来,可发现与前面的fingerprint一摸一样。
(lldb) po $x0
108
(lldb) po $x1
U0hBMjU2IGRpZ2VzdDogMjliMjUzNWNjMGM1ZTFlNzczYWEwNWFkMDQ1ZmI3MDNmZmJjNmU0OWU2ZWQwYTY2MWUxYzZhZGU3NDMwNzRiMw==
bt查看此时的调用栈,看到上游的调用链与后续应关注的函数调用地址。
(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = step out
* frame #0: 0x0000000100257730 Reveal`___lldb_unnamed_symbol19843 + 892
frame #1: 0x00000001000a1a58 Reveal`___lldb_unnamed_symbol9050 + 472
逐帧退栈,追踪至frame #1(地址0x1000A1A58-4)所调函数,即sub_1002573B4。
__text:0x1000A1A54 BL sub_1002573B4
在sub_1002573B4当中果然见到有关SHA256哈希的符号调用。
__text:0x1002573EC BL _$s9CryptoKit6SHA256VMa ; type metadata accessor for SHA256
__text:0x100257428 BL _$s9CryptoKit12SHA256DigestVMa ; type metadata accessor for SHA256Digest
; ...
我们先从sub_1002573B4前面部分开始看起,开头调用了下面几个函数。
__text:0x10025745C BL sub_1002577A4
__text:0x100257468 BL _getuid
__text:0x100257484 BL _$ss23CustomStringConvertibleP11descriptionSSvgTj ; dispatch thunk of CustomStringConvertible.description.getter
点进sub_1002577A4中,似乎是个获取网卡MAC地址的函数;至于getuid,顾名思义,大概用于获取当前用户UID。那就在LLDB中一验究竟,对地址0x10025745C设置断点,并重启。
(lldb) br s -a 0x10025745C
Breakpoint 4: where = Reveal`___lldb_unnamed_symbol19843 + 168, address = 0x000000010025745c
(lldb) r
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 4.1
frame #0: 0x000000010025745c Reveal`___lldb_unnamed_symbol19843 + 168
Reveal`___lldb_unnamed_symbol19843:
-> 0x10025745c <+168>: bl 0x1002577a4 ; ___lldb_unnamed_symbol19844
Target 0: (Reveal) stopped.
ni单步往下执行,过程中打印出sub_1002577A4返回值,为0x00001f88c300c968,与实际MAC地址68:c9:00:c3:88:1f相比,顺序是相反着的,这并非值错误,而是因为大小端存储方式差异导致的。
-> 0x10025745c <+168>: bl 0x1002577a4 ; ___lldb_unnamed_symbol19844
(lldb) ni
* thread #1, queue = 'com.apple.main-thread', stop reason = instruction step over
frame #0: 0x0000000100257460 Reveal`___lldb_unnamed_symbol19843 + 172
Reveal`___lldb_unnamed_symbol19843:
-> 0x100257460 <+172>: mov x27, x0
0x100257464 <+176>: mov x28, x1
0x100257468 <+180>: bl 0x100c6ad50 ; symbol stub for: getuid
Target 0: (Reveal) stopped.
(lldb) register read x0
x0 = 0x00001f88c300c968
$ ifconfig en0 | grep ether | awk '{print $2}'
68:c9:00:c3:88:1f
再看getuid函数,其返回值是0x00000000000001f5,正是当前用户UID的十六进制。
....
(lldb) ni
-> 0x100257468 <+180>: bl 0x100c6ad50 ; symbol stub for: getuid
(lldb) ni
-> 0x10025746c <+184>: stur w0, [x29, #-0x90]
(lldb) register read x0
x0 = 0x00000000000001f5
$ id -u
501
$ printf "0x%x" 501
0x1f5
往下接着运行,直至地址0x100257484,此处调用了Swift.CustomStringConvertible.description,单步运行并查看返回值,得到0x0000000000313035。
(lldb) ni
-> 0x100257484 <+208>: bl 0x100c6940c ; symbol stub for: dispatch thunk of Swift.CustomStringConvertible.description.getter : Swift.String
(lldb) ni
-> 0x100257488 <+212>: bl 0x100256bd8 ; ___lldb_unnamed_symbol19838
(lldb) register read x0
x0 = 0x0000000000313035
将该十六进制值按ASCII解码后,结果为105。结合前面提及的大小端存储差异,该函数作用是返回UID的字符串化结果。
$ echo 0x313035 | xxd -r -p
105
接下来我们将目光转向HashFunction.update,由于这是Swift的一个协议方法,没法像C或Objective-C函数那样直接按名下断点,所以需把断点设至该方法的跳转存根地址0x100C66C40。
__stubs:0x100C66C40 ; __int64 __fastcall dispatch thunk of HashFunction.update(bufferPointer:)(_QWORD, _QWORD, _QWORD, _QWORD)
__stubs:0x100C66C40 _$s9CryptoKit12HashFunctionP6update13bufferPointerySW_tFTj
__stubs:0x100C66C40 ; CODE XREF: sub_1002570B4+D4↑p
__stubs:0x100C66C40 ; sub_1002570B4+1B4↑p ...
__stubs:0x100C66C40 ADRP X16, #_$s9CryptoKit12HashFunctionP6update13bufferPointerySW_tFTj_ptr@PAGE
__stubs:0x100C66C44 LDR X16, [X16,#_$s9CryptoKit12HashFunctionP6update13bufferPointerySW_tFTj_ptr@PAGEOFF]
__stubs:0x100C66C48 BR X16 ; __imp__$s9CryptoKit12HashFunctionP6update13bufferPointerySW_tFTj
(lldb) br dis
All breakpoints disabled. (5 breakpoints)
(lldb) br s -a 0x100C66C40
Breakpoint 6: where = Reveal`symbol stub for: dispatch thunk of CryptoKit.HashFunction.update(bufferPointer: Swift.UnsafeRawBufferPointer) -> (), address = 0x0000000100c66c40
(lldb) r
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 6.1
frame #0: 0x0000000100c66c40 Reveal`dispatch thunk of CryptoKit.HashFunction.update(bufferPointer: Swift.UnsafeRawBufferPointer) -> ()
Reveal`dispatch thunk of CryptoKit.HashFunction.update(bufferPointer: Swift.UnsafeRawBufferPointer) -> ():
-> 0x100c66c40 <+0>: adrp x16, 902
Reveal`dispatch thunk of CryptoKit.HashFunction.finalize() -> τ_0_0.Digest:
0x100c66c4c <+0>: adrp x16, 902
Target 0: (Reveal) stopped.
待触发中断,打印出HashFunction.update的输入buffer,发现寄存器x0指向的内存地址0x16fdfdff8,之中的内容正是MAC地址与UID的拼接。
(lldb) register read x0
x0 = 0x000000016fdfdff8
(lldb) memory read $x0
0x16fdfdff8: 68 c9 00 c3 88 1f 35 30 31 00 00 00 00 00 00 00
0x16fdfe008: 44 00 6f a3 ac a0 10 ad 50 e0 df 6f 01 00 00 00
查看此时调用栈,与base64EncodedString调用栈对比,发现分水岭在于frame #3(0x00000001000a1a58),即base64EncodedString调用栈中的frame #1,这也证明了前面对该处所调函数sub_1002573B4进行分析是正确的。
(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 6.1
* frame #0: 0x0000000100c66c40 Reveal`dispatch thunk of CryptoKit.HashFunction.update(bufferPointer: Swift.UnsafeRawBufferPointer) -> ()
frame #1: 0x000000010025718c Reveal`___lldb_unnamed_symbol19839 + 216
frame #2: 0x00000001002576bc Reveal`___lldb_unnamed_symbol19843 + 776
frame #3: 0x00000001000a1a58 Reveal`___lldb_unnamed_symbol9050 + 472
....
最后,对HashFunction.finalize设置断点,同样设置在其跳转存根地址,是0x100C66C4C。重启应用,待抵达其入口地址,finish执行完至调用返回点。
(lldb) br dis
All breakpoints disabled. (6 breakpoints)
(lldb) br s -a 0x100C66C4C
Breakpoint 7: where = Reveal`symbol stub for: dispatch thunk of CryptoKit.HashFunction.finalize() -> τ_0_0.Digest, address = 0x0000000100c66c4c
(lldb) r
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 7.1
frame #0: 0x0000000100c66c4c Reveal`dispatch thunk of CryptoKit.HashFunction.finalize() -> τ_0_0.Digest
Reveal`dispatch thunk of CryptoKit.HashFunction.finalize() -> τ_0_0.Digest:
-> 0x100c66c4c <+0>: adrp x16, 902
Reveal`dispatch thunk of CryptoKit.HashFunction.init() -> τ_0_0:
0x100c66c58 <+0>: adrp x16, 902
Target 0: (Reveal) stopped.
(lldb) finish
* thread #1, queue = 'com.apple.main-thread', stop reason = step out
frame #0: 0x00000001002576dc Reveal`___lldb_unnamed_symbol19843 + 808
Reveal`___lldb_unnamed_symbol19843:
-> 0x1002576dc <+808>: ldur x8, [x29, #-0xd0]
Target 0: (Reveal) stopped.
finalize()的返回值被写入栈上的临时缓冲区,读取当前栈指针sp所指向的内存,即可看到该缓冲区的内容,正是此前Base64解码后得到的那32字节SHA256摘要。
(lldb) memory read $sp
0x16fdfe050: 29 b2 53 5c c0 c5 e1 e7 73 aa 05 ad 04 5f b7 03
0x16fdfe060: ff bc 6e 49 e6 ed 0a 66 1e 1c 6a de 74 30 74 b3
经过动态追踪与内存比对,我们证实了先前对指纹生成算法的猜想:将MAC地址与当前用户UID的字符串形式直接拼接,形成一个9字节的二进制数据块;然后对该数据块进行SHA256哈希,得到32字节摘要;再将摘要转换为十六进制字符串,并添加“SHA256 digest: ”前缀;最后对整段文本Base64编码,生成最终的fingerprint。
凭证植入
在前面LLDB调试运行应用的过程中,相信已经见到过上面的Keychain弹窗,这一现象表明应用通过网络获取到授权凭证信息后,将其存储至Keychain当中了。
使用security结合find-generic-password子命令可从Keychain中一窥凭证究竟,如下password中的access_token就是来自网络响应中的access_token。
$ sudo security find-generic-password -s "com.ittybittyapps.Reveal2" -a "credential" -g
keychain: "/Users/$USER/Library/Keychains/login.keychain-db"
version: 512
class: "genp"
attributes:
0x00000007 <blob>="com.ittybittyapps.Reveal2"
0x00000008 <blob>=<NULL>
"acct"<blob>="credential"
"cdat"<timedate>=0x32303235303931393038333331335A00 "20250919083313Z\000"
"crtr"<uint32>=<NULL>
"cusi"<sint32>=<NULL>
"desc"<blob>=<NULL>
"gena"<blob>=<NULL>
"icmt"<blob>=<NULL>
"invi"<sint32>=<NULL>
"mdat"<timedate>=0x32303235303931393038333332325A00 "20250919083322Z\000"
"nega"<sint32>=<NULL>
"prot"<blob>=<NULL>
"scrp"<sint32>=<NULL>
"svce"<blob>="com.ittybittyapps.Reveal2"
"type"<uint32>=<NULL>
password: "{"state":"present","content":{"access_token":"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE3NTgxODY4NjEuMDYwNjQ5OSwiZXhwIjoxNzU4MTg3MTYxLjA2MDY1LCJlbWFpbCI6InN2bm55NzdAZ21haWwuY29tIiwibGljZW5zZV9wYXRoIjoiXC9saWNlbnNlc1wvMjREQjBCM0ItNDhCQi0xNEE2LTkxQ0QtMDI4OTA4NDkyQ0UzIiwiY2xpZW50X2V4cGlyZXNfYXQiOjE3NTg0NDYzNjEuMDYwNjUsImxpY2Vuc2Vfc3R5bGUiOjAsIm9yZ2FuaXphdGlvbl9zdHlsZSI6MCwiaWRlbnRpZmllciI6IjRpR0R4bFBjaE1kMmsrK3lzOWVpTzE4eFJOYUdyV0xGWjRBeWRCaUJyOT0iLCJpc19zdWJzY3JpcHRpb25fdXNhYmxlIjpmYWxzZSwic3ViIjoiRDcxOEU1RDctOUE2OC0xNEY0LUNFRDEtNENFMkUyNEQwNTkwIiwib3JnYW5pemF0aW9uX25hbWUiOiJzdm5ueTc3QGdtYWlsLmNvbSIsImZpbmdlcnByaW50IjoiVTBoQk1qVTJJR1JwWjJWemREb2dNamxpTWpVek5XTmpNR00xWlRGbE56Y3pZV0V3TldGa01EUTFabUkzTURObVptSmpObVUwT1dVMlpXUXdZVFkyTVdVeFl6WmhaR1UzTkRNd056UmlNdz09IiwibGljZW5zZV9leHBpcnkiOjE3NTkzOTY0MDkuMDExOTU3MiwiaXNzIjoiUmV2ZWFsIGJ5IE1hbnRlbCJ9.OPReNgGoxsJora0jvmLlq37rbOqNmQiHNtgFLwFZ0RBD5wiLz6xkXpPHWlk6Ot5Y8wisld_-F6muEwEeqPyPcGKWNuD_Ot4bnG8uYx4rUx-CeiW33saDDl-mMX1GCF50917wWJH00zYzfZtZMIZMwupsUutUswDm9S6DFwUyArvXNBagUc9Rvi6-X1yBm2BHBLvOkp8p726mMQphk8MJ1GcxiwbuAIcq3Wg2FxZronT234XG2gWOAzYkubKF4GbTP6kjlpt3jSGoEk0qNKwsa6mK7WNicIVARy4-6_FzGjPvzNx9O9qsHPy8LE-fad6YaG1JmikA-XQw9QsdITttjK","refresh_token":"e8991018349asc3c12dd4812d2345650fdfdaad585ed0cdc1eb9010397684ec6"}}"
既然能够通过security命令查看应用的凭证信息,那必然也可以通过security来植入凭证,用到的子命令是add-generic-password。不过,需先构造出access_token字段值,基于前一节对指纹生成算法的分析,先把本机fingerprint生成出来。
$ ifconfig en0 | grep ether | awk '{print $2}'
68:c9:00:c3:88:1f
$ printf "%d" `id -u` | xxd -p
353031
$ printf '\x68\xc9\x00\xc3\x88\x1f\x35\x30\x31' | shasum -a 256
29b2535cc0c5e1e773aa05ad045fb703ffbc6e49e6ed0a661e1c6ade743074b3 -
$ echo -n 'SHA256 digest: 29b2535cc0c5e1e773aa05ad045fb703ffbc6e49e6ed0a661e1c6ade743074b3' | base64
U0hBMjU2IGRpZ2VzdDogMjliMjUzNWNjMGM1ZTFlNzczYWEwNWFkMDQ1ZmI3MDNmZmJjNmU0OWU2ZWQwYTY2MWUxYzZhZGU3NDMwNzRiMw==
然后在原有的试用授权信息基础之上修改关键字段值,无非是将0改为1、false改为true、小值改大。
{
"is_subscription_usable": true,
"fingerprint": "U0hBMjU2IGRpZ2VzdDogMjliMjUzNWNjMGM1ZTFlNzczYWEwNWFkMDQ1ZmI3MDNmZmJjNmU0OWU2ZWQwYTY2MWUxYzZhZGU3NDMwNzRiMw==",
"exp": 253392455349,
"license_path": "/licenses/24DB0B3B-48BB-14A6-91CD-028908492CE3",
"iss": "Reveal by Mantel",
"organization_style": 1,
"license_expiry": 253392455349,
"sub": "D718E5D7-9A68-14F4-CED1-4CE2E24D0590",
"organization_name": "F",
"client_expires_at": 253392455349,
"iat": 1757680656,
"license_style": 1,
"identifier": "4iGDxlPchMd2k++ys9eiO18xRNaGrWLFZ4AydBiBr9=",
"email": "K'ed_by_0xf"
}
中间构造access_token的其他操作省略,最终的凭证植入命令如下。
$ sudo security add-generic-password -s "com.ittybittyapps.Reveal2" -a "credential" -U -w '{"state": "present", "content": {"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpc19zdWJzY3JpcHRpb25fdXNhYmxlIjp0cnVlLCJmaW5nZXJwcmludCI6IlUwaEJNalUySUdScFoyVnpkRG9nTWpsaU1qVXpOV05qTUdNMVpURmxOemN6WVdFd05XRmtNRFExWm1JM01ETm1abUpqTm1VME9XVTJaV1F3WVRZMk1XVXhZelpoWkdVM05ETXdOelJpTXc9PSIsImV4cCI6MjUzMzkyNDU1MzQ5LCJsaWNlbnNlX3BhdGgiOiIvbGljZW5zZXMvMjREQjBCM0ItNDhCQi0xNEE2LTkxQ0QtMDI4OTA4NDkyQ0UzIiwiaXNzIjoiUmV2ZWFsIGJ5IE1hbnRlbCIsIm9yZ2FuaXphdGlvbl9zdHlsZSI6MSwibGljZW5zZV9leHBpcnkiOjI1MzM5MjQ1NTM0OSwic3ViIjoiRDcxOEU1RDctOUE2OC0xNEY0LUNFRDEtNENFMkUyNEQwNTkwIiwib3JnYW5pemF0aW9uX25hbWUiOiJGIiwiY2xpZW50X2V4cGlyZXNfYXQiOjI1MzM5MjQ1NTM0OSwiaWF0IjoxNzU3NjgwNjU2LCJsaWNlbnNlX3N0eWxlIjoxLCJpZGVudGlmaWVyIjoiNGlHRHhsUGNoTWQyaysreXM5ZWlPMTh4Uk5hR3JXTEZaNEF5ZEJpQnI5PSIsImVtYWlsIjoiSydlZF9ieV8weGYifQ==.dummy-signature", "refresh_token": "6"}}'
由于应用每次启动之初,都会携带access_token请求activation/verify接口来进行激活验证。因此,还需屏蔽其网络通信,确保应用只依赖植入的本地凭证。
网络钩子
回顾前面所有,蓦然回首,又想到一种相对简单、优雅的破解方式,即最常见的中间人劫持,网络Hook。通过Hook -[NSURLSession dataTaskWithRequest:completionHandler:]方法,掌控应用与服务端全部的网络通信,并实现以下两点。
- 对于首次激活:从请求中提取出fingerprint参数值,将原始响应篡改为有效授权信息的响应,以及篡改状态码为200。
- 对于后续验证:直接Drop掉激活验证请求,应用在得不到回应的情况下,会默认本地凭证依然有效。
当然,这一切依然需要建立在废除签名验证的基础之上。介于文章篇幅,Hook代码就不放出。
惊喜彩蛋
无论是基于前述大量分析而施行的凭证植入,还是根据上述逻辑,将编写的网络Hook代码编译成Dylib注入应用中,殊途同归,最终目的都是一致的。不过,彩蛋环节却是后者独有的。
现在,打开应用,确保Mac的声音已开启。在弹出的激活窗口中输入任意Key,点击激活的瞬间,一阵欢庆声顿时响起,热闹非凡,用以庆祝破解成功再合适不过了。