lldb 调试某数据库客户端 + license 逆向 + python3 keygen(win/mac 通杀)
1. 关于
之前做过该软件的分析, 不过似乎作者与我心有灵犀, 之前的 patch 已经过期了;
今天试一下新的方案, 逆向 license 文件的加解密, 并且完成 keygen
2. license 逆向
ida 拖入文件, 从关键字 license 入手, 查找关键字定位到了该字符串
__cstring:0000000100735D60 aLicenseFileIsI db 'License file is invalid ',0
查看 Ref, 定位到了加载 license 的函数里面
可以看到有些关键地方,我们挨个去分析
2.1. 调用 fileExistsAtPath: 查看 license 文件是否存在
__text:0000000100144B6D mov rsi, cs:selRef_fileExistsAtPath_
__text:0000000100144B74 mov rdi, rbx
__text:0000000100144B77 mov rdx, r13
__text:0000000100144B7A call _objc_msgSend
lldb 加载程序, 在 call fileExistsAtPath
处下断点, r 重启
断下来后, 输出寄存器 r13 的对象, 查看 license 的具体路径
br s -a 0x100144B7A
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 2.1
frame #0: 0x0000000100144b7a TablePlus`___lldb_unnamed_symbol8433 + 362
TablePlus`___lldb_unnamed_symbol8433:
-> 0x100144b7a <+362>: callq 0x1006d7636
0x100144b7f <+367>: movl %eax, %r12d
0x100144b82 <+370>: movq 0x692cf7(%rip), %r14
0x100144b89 <+377>: movq %rbx, %rdi
Target 0: (TablePlus) stopped.
(lldb) po $r13
/Users/voidm/Library/Application Support/com.tinyapp.TablePlus/.licensemac
由此我们得到了 license 绝对路径:
'/Users/voidm/Library/Application Support/com.tinyapp.TablePlus/.licensemac'
2.1. 调用 RNCryptor 相关库函数进行解密
利用上面获取到的 license 文件路径, 我们试着伪造一个 license
不过,首先得了解下该算法, 我问了我大哥(chatgpt), 跟我介绍了下:
RNCryptor.DecryptorV3 是 RNCryptor 库中的一个解密器类,用于解密符合 RNCryptor v3 格式的加密数据。
RNCryptor 是基于 AES-256-CBC 和 PBKDF2 进行加密和解密的跨平台库,支持 iOS(Objective-C & Swift)、macOS 和其他平台。
通过查看<该库源码>
发现其底层调用了 macos 下 CommonCryptor 库的相关函数
NAME
CCCryptorCreate, CCryptorCreateFromData, CCCryptorRelease,
CCCryptorUpdate, CCCryptorFinal, CCCryptorGetOutputLength,
CCCryptorReset, CCCrypt -- Common Cryptographic Algorithm Interfaces
LIBRARY
These functions are found in libSystem.
SYNOPSIS
#include <CommonCrypto/CommonCryptor.h>
之后查了些<资料>, 找到了该算法生成 license 的基本文件格式, 如下
Byte: | 0 | 1 | 2-9 | 10-17 | 18-33 | <- ... -> | n-32 - n |
Contents: | version | options | encryptionSalt | HMACSalt | IV | ... ciphertext ... | HMAC |
version (1 byte): Data format version. Currently 3.
options (1 byte): bit 0 - uses password
encryptionSalt (8 bytes): iff option includes "uses password"
HMACSalt (8 bytes): iff options includes "uses password"
IV (16 bytes)
ciphertext (variable) -- Encrypted in CBC mode
HMAC (32 bytes)
看不懂没关系, 我来翻译一下:
version: 第一个字节,代表算法版本号, 既然是 DecryptorV3 就是 固定 0x3
options: 第二个字节,是否使用 password, 先默认它是把, 我们写成 0x1
encryptionSalt: 八个字节的 encryptionSalt,用于 KEY 派生的 PBKDF2 盐值, 我们随机造一个
01 00 00 00 00 00 00 00
HMACSalt: 八个字节的 HMACSalt,用于 HMAC KEY 派生的 PBKDF2 盐值, 我们随便造一个
02 00 00 00 00 00 00 00
IV: 十六个字节的 IV,AES CBC 初始向量, 我们随便造一个
03 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
ciphertext: 可变长度的 ciphertext, 经过 AES-256-CBC
加密的实际内容, 可能包含 PKCS7
填充, 也就是 license 的本体了
这个 ciphertext 先随便伪造一些
04 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
HMAC: 三十二长度的 HMAC, SHA256 校验码,用于完整性验证
这个 HMAC 也先随便伪造一些
05 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
至此, 临时的 license 文件伪造好了, 写入到 licese 路径下
2.1.1. encryptionSalt 验证,获取 password
该算法第一步会用 password + salt 来生成 key, 这块会用到 CCKeyDerivationPBKDF
函数 , 我们下断
CCKeyDerivationPBKDF(CCPBKDFAlgorithm algorithm,
const char *password, size_t passwordLen,
const uint8_t *salt, size_t saltLen,
CCPseudoRandomAlgorithm prf, uint rounds,
uint8_t *derivedKey, size_t derivedKeyLen)
br s -n CCKeyDerivationPBKDF
断下来后,可以看到 algorithm 算法参数 与 rounds 值
通过观察参数 salt , 看到与我们伪造的一致, 之后输出参数 password , 获取到了该加密用到的密码 (该密码在同一个软件中通常是固定存在的)
(lldb) br s -n CCKeyDerivationPBKDF
Breakpoint 3: where = libcommonCrypto.dylib`CCKeyDerivationPBKDF, address = 0x00007ff81e077142
之后记录下改函数生成的 key, 参数的地址为 rsp+0x10, 先记录下地址,
然后 finish 执行完函数后, 在获取地址内容,因为该参数是个引用,需要在解一层地址,
最后获取到 由 salt + password 生成的 key 如下:
665e8e6c6bf0befde5c30062f608db27be0443d59f2e410f4171933fb837565f
(lldb) p/x $rsp+0x10
(unsigned long) 0x00007ff7bfefdbc8
(lldb) finish
(lldb) x/8xg 0x00007ff7bfefdbc8
0x7ff7bfefdbc8: 0x0000600001da5c60 0x0000000000000020
(lldb) mem read 0x00600001da5c60 -c 32
0x600001da5c60: 66 5e 8e 6c 6b f0 be fd e5 c3 00 62 f6 08 db 27 f^.lk......b...'
0x600001da5c70: be 04 43 d5 9f 2e 41 0f 41 71 93 3f b8 37 56 5f ..C...A.Aq.?.7V_
2.1.2. HMACSalt 验证
第二步会用 password + HMACSalt 来生成 hmac_key, 同前面一样, 下断 CCKeyDerivationPBKDF
断下来后, 通过观察参数 HMACSalt , 看到与我们伪造的一致, 然后输出 password , 也与上面得到的 password 一致
之后用同样的方式记录下生成的 hmac_key
获取到 由 HMAC Salt + password 生成的 hmac_key 如下:
da049e62d7dcd964fe620f3388573cc88f6ff1e2d4516db7fc374b2f6ff69602
(lldb) p/x $rsp+0x10
(unsigned long) 0x00007ff7bfefdbc8
(lldb) x/8xg 0x00007ff7bfefdbc8
0x7ff7bfefdbc8: 0x0000600001da5c60 0x0000000000000020
0x7ff7bfefdbd8: 0x00000001000974b7 0x0000000000000022
0x7ff7bfefdbe8: 0x0000000000000010 0x0000001000000000
0x7ff7bfefdbf8: 0x0000600001da5c40 0x00007ff7bfefdc50
(lldb) finish
Process 61779 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = step out
frame #0: 0x00000001000974fd TablePlus`___lldb_unnamed_symbol5091 + 189
TablePlus`___lldb_unnamed_symbol5091:
-> 0x1000974fd <+189>: addq $0x20, %rsp
0x100097501 <+193>: movl %eax, %r12d
0x100097504 <+196>: movq %r14, %rdi
0x100097507 <+199>: callq 0x1006d782e
Target 0: (TablePlus) stopped.
(lldb) mem read 0x00600001da5c60 -c 32
0x600001da5c60: da 04 9e 62 d7 dc d9 64 fe 62 0f 33 88 57 3c c8 ...b...d.b.3.W<.
0x600001da5c70: 8f 6f f1 e2 d4 51 6d b7 fc 37 4b 2f 6f f6 96 02 .o...Qm..7K/o...
2.1.3 生成 ciphertext
接着看代码, license 解密成功后, 会调用 JSON相关函数转换成 TBLicenseModel 对象
由此得知, 我们的 license Ciphertext 解密后是 json 字符串
在 ida 分析 TBLicenseModel 可以得到以下 ivar 属性
__const:00000001006E8868 _OBJC_IVAR_$__TtC9TablePlus14TBLicenseModel_sign dq 10h
__const:00000001006E8868
__const:00000001006E8870 _OBJC_IVAR_$__TtC9TablePlus14TBLicenseModel_email dq 20h
__const:00000001006E8870
__const:00000001006E8878 _OBJC_IVAR_$__TtC9TablePlus14TBLicenseModel_deviceID dq 30h
__const:00000001006E8878
__const:00000001006E8880 _OBJC_IVAR_$__TtC9TablePlus14TBLicenseModel_licenseKey dq 40h
__const:00000001006E8880
__const:00000001006E8888 _OBJC_IVAR_$__TtC9TablePlus14TBLicenseModel_purchasedAt dq 50h
__const:00000001006E8888
__const:00000001006E8890 _OBJC_IVAR_$__TtC9TablePlus14TBLicenseModel_updatesAvailableUntil dq 60h
__const:00000001006E8890
__const:00000001006E8898 _OBJC_IVAR_$__TtC9TablePlus14TBLicenseModel_nextChargeAt dq 70h
根据 TBLicenseModel 的 ivar 属性来拼接一个字符串
我们可以利用 key + iv 来做一个 aes cbc 加密字符串, 生成 ciphertext
生成之后,替换我们 license 文件中的 ciphertext
然后 lldb r 重启
2.1.4 生成 HMAC
HMAC 是用来验证文件的完整性的,用于数据完整性校验,如果 HMAC 不匹配,则数据可能被篡改
算法如下
header = version + options + encryptionSalt + HMACSalt + IV
hmac_message = header + ciphertext
HMAC = HMAC-SHA256(hmac_key, hmac_message)
取到之后,替换我们 license 文件中的的 hmac
9d79987595a230de47d90f8d7842b7821696a7517a8a2d8ce98be470ce3e2a52
致此, license 伪造如下:
然后 lldb 重启,继续调试
2.2 验证
解密后的 json 字符串会调用 JSONDecoder.decode 转成 LicenseModel 对象, 我们 在 0000000100144F68 下断
br s -a 0x100144F68
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 4.1
frame #0: 0x0000000100144f68 TablePlus`___lldb_unnamed_symbol8433 + 1368
TablePlus`___lldb_unnamed_symbol8433:
-> 0x100144f68 <+1368>: callq 0x1006d5c8c
0x100144f6d <+1373>: movq %r14, %rbx
0x100144f70 <+1376>: testq %r12, %r12
0x100144f73 <+1379>: je 0x100144fb7
Target 0: (TablePlus) stopped.
(lldb)
断下来后, 输出前几个参数调试下 ,
可以看到第一个参数是一个 TBLicenseModel 类的模版,
第二个参数未知
第三个参数 Foundation.__DataStorage
看着像是我们要验证的数据
(lldb) po $rdi
TablePlus.TBLicenseModel
(lldb) po $rsi
1060856922112
(lldb) reg r rsi
rsi = 0x000000f700000000
(lldb) po $rdx
4611791571565053824
(lldb) reg r rdx
rdx = 0x4000600001468780
(lldb) po 0x600001468780
Foundation.__DataStorage
class __DataStorage {
var isa: objc_class*
var refCount: UInt64
var _bytes: UnsafeMutableRawPointer?
var _length: UInt64
var _capacity: UInt64
var _offset: UInt64
var _deallocator: ((UnsafeMutableRawPointer, Int64) -> Void)?
var _needToZero: Bool
}
(lldb) x/8xg 0x600001468780
0x600001468780: 0x00007ff852ef01e0 0x0000000000000003
0x600001468790: 0x00007feeedd21c60 0x00000000000000f7
0x6000014687a0: 0x0000000000000140 0x0000000000000000
0x6000014687b0: 0x0000000000000000 0x0000000000000000
__DataStorage
是 swift 的 类, 可以看到有下面几个属性
通过 lldb 命令, 查看指针的一些偏移, 可以看到第三个为 char* 数组, 第四个为长度, 读取该地址内存, 可以看到 我们的 license 成功了解密出来了
mem read 0x00007feeedd21c60 -c 0xf7
至此 license 解密完成
lldb r 重启程序, 然后直接运行, 看效果
3. keygen
利用前面获取到的各种, 我们用 python3 基于 pycryptodomex 库手写一个 keygen, 来方便快捷生成 license 文件
# pip3 install pycryptodomex
class CryptoBase:
def __init__(self):
self.schema_version = 3
self.aes_mode = AES.MODE_CBC
self.options = 1
self.hmac_includes_header = True
self.hmac_includes_padding = False
self.hmac_algorithm = SHA256
self.key_len = 32
class Encryptor(CryptoBase):
def encrypt(self, plaintext, password):
salt = os.urandom(8)
hmac_salt = os.urandom(8)
iv = os.urandom(16)
key = self.generate_key(salt, password)
hmac_key = self.generate_key(hmac_salt, password)
cipher = AES.new(key, AES.MODE_CBC, iv)
ciphertext = cipher.encrypt(pad(plaintext, AES.block_size))
# schema + options + salt + hmac_salt + iv
header = self.assemble_header(salt, hmac_salt, iv)
print_hex(key, "key") # schema (1 byte)
hmac_message = header + ciphertext
hmac = self.generate_hmac(hmac_message, hmac_key)
encrypted_data = header + ciphertext + hmac
return encrypted_data
def generate_key(self, salt, password):
return hashlib.pbkdf2_hmac('SHA1', password.encode('utf-8'), salt, 10000, dklen=self.key_len)
def assemble_header(self, salt, hmac_salt, iv):
schema = struct.pack('B', self.schema_version)
options = struct.pack('B', self.options)
return schema + options + salt + hmac_salt + iv
def generate_hmac(self, message, key):
hmac = HMAC.new(key, digestmod=self.hmac_algorithm)
hmac.update(message)
return hmac.digest()
if __name__ == '__main__':
licenseFile = '/Users/voidm/Library/Application Support/com.tinyapp.TablePlus/.licensemac'
password = "马赛克"
plaintext = b'{"sign":"12345678901234567890123456789012345678901234567890","email":"marlkiller","deviceID":"ee4f1d1890b4eb49a5a4d7f195ca8b67","licenseKey":"licenseKey","purchasedAt":"2026-12-30","nextChargeAt":521,"updatesAvailableUntil":"2026-12-30"}'
encryptor = Encryptor()
encrypted_data = encryptor.encrypt(plaintext, password)
write_encrypted_data_to_file(encrypted_data, licenseFile)
print("Mac Encrypt: " + encrypted_data.hex())
4. 后记
项目已经打包 github,可以直接用 xcode 打开 :
https://github.com/marlkiller/dylib_dobby_hook
git clone https://github.com/marlkiller/dylib_dobby_hook.git
cd script
sudo bash auto_hack.sh
-
- 软件有接口请求验证, 自行断网测试,这里不做讨论
-
- mac/win password 不一致, 但是算法通杀.
-
- deviceId 的生成规则<参考这里>