前记
TablePlus 是我很喜欢的 macos 平台的数据库工具...
今天我们来基于 dylib 注入来分析一下。
环境
- ida|hopper
- xcode 15
- TablePlus Version 5.8.2 (528) [x86/arm 通杀]
分析 x86
首先软件的免费版本,是有很多限制的,
比如 只能打开俩个 tab 页
我们先从这里入手。
hopper 打开, 字符串搜索, 找到如下:
0000000100731c70 db "Free Trial limited 2 tabs", 0
x 一下, 有四个引用, 我们找一个相对调用链路图短的 分析。
就第二个吧。
切换到假码模式, 我们看到, 这块有个关键判断
qword_1008daaf0 == 0x0 的话 ,会弹出 Tab 限制的对话框
聪明的同学可能想到了, 直接写入内存 0x1, 但是 , 我试过了, 不行的..
qword_1008daaf0 这个引用是一个对象,对象的属性取不到程序会出异常奔溃 !!
00000001008daaf0 db 0x00 ; '.'
换个工具, 我们用 ida 打开, 对该内存地址下一个写入断点 (之所以换工具, 是因为我不知道怎么用 hopper 下写入断点)
下好断点后, run 运行
__text:00000001002FA1F1 mov rbp, rsp
__text:00000001002FA1F4 call sub_10014AF90
__text:00000001002FA1F9 mov cs:qword_1008DAAF0, rax
__text:00000001002FA200 pop rbp
__text:00000001002FA201 retn
可以看到, 执行函数 sub_10014AF90 返回 rax, 然后 赋值给了qword_1008daaf0 ;
我们分析下 sub_10014AF90 这个函数。;
函数太长, 我们直接看 return, 由前面以知, 返回0是会弹框, 所以我们忽略掉 176 216 行的 return 0;
着重看后面的 return v38;
v38 = v36.initWithDictionary
v36 = objc_allocWithZone(&OBJC_CLASS___LicenseModel); // 这一行实例化 LicenseModel 对象,很关键 !!
v36 = objc_allocWithZone(&OBJC_CLASS___LicenseModel);
v37 = (void *)_sSD10FoundationE19_bridgeToObjectiveCSo12NSDictionaryCyF(
v35,
&_ss11AnyHashableVN,
(char *)&_sypN + 8,
&_ss11AnyHashableVSHsWP);
swift_bridgeObjectRelease(v35);
v38 = objc_msgSend(v36, "initWithDictionary:", v37);
objc_release(v37);
v39 = _sSS10FoundationE19_bridgeToObjectiveCSo8NSStringCyF(0x2E676E6964616F4CLL, 0xEA00000000002E2ELL);
objc_msgSend(v38, "setUpdatesAvailableUntil:", v39);
sub_100025820(v65);
v40 = *(void (__fastcall **)(char *, __int64))(v72 + 8);
v41 = v73;
v40(v69, v73);
v40(v68, v41);
sub_1000255A0(v67, v70);
sub_1000255A0(InitializedObjCClass, v66);
swift_bridgeObjectRelease(v64);
((void (__fastcall *)(__int64))objc_release)(v39);
return v38;
简单了解了下, initWithDictionary 是将 map 转换成对象, 我们用 hopper 把程序的 header 文件导出来, 看看 LicenseModel 都有哪些属性.
这就简单明白了, 我们直接搞个函数,把 LicenseModel hook 过去
hook 之前 先把这几个用到的头文件扔到我们项目里
具体 HOOK 代码,如下
(具体的 hook 实现参考帖子 https://www.52pojie.cn/thread-1880510-1-1.html) :
注意 rbx 也就是 licenseModel 我写成了常量, 防止内存泄漏, 而且程序启动时,会先执行一边该函数 来生成 rbx 对象常量, 之后在 hook
const LicenseModel *rbx;
int (*sub_10014AF90Ori)();
id sub_10014AF90New(int arg0, int arg1, int arg2, int arg3){
LicenseModel *r12 = [[NSClassFromString(@"LicenseModel") alloc] init];
NSDictionary *propertyDictionary = @{
@"sign": @"fuckSign",
@"email": @"marlkiller@voidm.com",
@"deviceID": @"fuckDeviceId",
@"purchasedAt": @"2999-01-16",
@"nextChargeAt": @(9999999999999), // Replace with the actual double value
@"updatesAvailableUntil": @"2999-01-16" // Replace with the actual value
};
if (rbx==nil){
rbx = [r12 initWithDictionary:propertyDictionary];;
}
return rbx;
}
main(){
sub_10014AF90New(1,2,3,4);
intptr_t _sub_10014AF90 = [Constant getBaseAddr:0] + 0x10014AF90;
DobbyHook(_sub_10014AF90, sub_10014AF90New, (void *)&sub_10014AF90Ori);
}
之后 dylib 注入, 重启程序, 发现 license 信息是有了, 但是还是有弹框..而且 程序界面依然有 free trial 字样
好消息是, 还是有弹框, 我们可以回到最初的
“Free Trial limited 2 tabs”
地方继续分析
然后: 下断,弹框,断下来了
可以看到 这里还有一个判断,
test bl, 1 , // 如果 bl 不等于 1, 则直接跳弹框
bl 等于 ebx 等于 eax 等于 函数 sub_100059E70 的返回值
接着我们分析下函数 sub_100059E70 的 代码:
这代码可写的太好了, 短小精干, 通俗易懂,(看起来像是校验格式的, 第三个参数肯能是设备id 或者激活码之类的? 有空可以在研究研究)
别管他啥逻辑, 我们直接 hook 返回 1 试试
// 前端的 hook, 我就懒得在贴在这了
int (*sub_100059E70Ori)();
bool sub_100059E70New(int arg0, int arg1, int arg2, int arg3, int arg4){
return 0x1;
}
main(){
intptr_t _sub_100059E70 = [Constant getBaseAddr:0] + 0x100059E70;
DobbyHook(_sub_100059E70, sub_100059E70New, (void *)&sub_100059E70Ori);
}
hook ret 1 后 软件基本可用了, 但是还有一些小 bug..
然后我们在稍微多看看 0x100059E70 这个函数;
这个函数 0x100059E70 是教研 deviceId 的, 简单跟踪下 可以获取到真实的 设备id ,
在 函数 100059E70 中 ,device id 来自 mov rsi , [rdx+0x28],
获取到对象地址之后, 通过反射, 可以获取到 class , 以及类的属性
可以看到对象为 __StringStorage, 对象的 description 为设备 ID ,
如果是 用 lldb 调试的话, 也可以直接 po rsi 来获取对象的 description, 即设备 id
+ (void)inspectObjectWithAddress:(void *)address {
id object = (__bridge id)address;
// LicenseModel *license = (__bridge LicenseModel *)addressPtr;
// 获取对象的十六进制地址
uintptr_t ptrValue = (uintptr_t)address;
NSLog(@">>>>>> Address: 0x%lx", ptrValue);
// 获取对象的类名
NSString *className = NSStringFromClass([object class]);
NSLog(@">>>>>> Class: %@", className);
// %@ 格式说明符将其作为对象进行输出。在此情况下,NSLog 将会调用对象的 description 方法来获取其字符串表示形式,并将其输出到控制台。
NSLog(@">>>>>> className.description: %@", address);
NSString *objectDescription = [object description];
NSLog(@">>>>>> Object Description: %@", objectDescription);
// 获取对象的属性与值
unsigned int count;
objc_property_t *properties = class_copyPropertyList([object class], &count);
for (unsigned int i = 0; i < count; i++) {
objc_property_t property = properties[i];
NSString *propertyName = [NSString stringWithUTF8String:property_getName(property)];
id propertyValue = [object valueForKey:propertyName];
NSLog(@">>>>>> Property: %@, Value: %@", propertyName, propertyValue);
}
free(properties);
}
>>>>>> Class: Swift.__StringStorage
>>>>>> className.description: ee4f1d1890b4eb49a5a4d7f195ca8b67
>>>>>> deviceId: ee4f1d1890b4eb49a5a4d7f195ca8b67
或者也可以直接 对象地址+0x20 的 偏移来获取 设备 ID
NSString *deviceId = [NSString stringWithCString:addressPtr+0x20 encoding:NSUTF8StringEncoding];
这里获取到 我本机 设备id 为 : 88548e5a38eeee04e89c5621ba04bc7e
而 第一个 hook 随便写了一个 fuckDeviceId, 我们替换成 88548e5a38eeee04e89c5621ba04bc7e
如果 设备 id 正确的话, 第二个 hook 其实就没必要 hook 了;
当然写死 deviceId 的话, 别人用的时候 还是会出问题;
设备 id 不匹配, 可能会有一些奇怪的问题, 比如 sql 会添加莫名其妙的引号...
这里我们稍微改改我们 hook 100059E70 函数的时候, 获取真实的deviceId ,然后赋值给我们的 licenseModel, 然后 在执行 100059E70Ori
bool sub_100059E70New(uint64_t arg0, uint64_t arg1, uint64_t arg2, uint64_t arg3, uint64_t arg4){
if (_rbx!=nil && _rbx.deviceID==@""){
// mov rsi, qword [rdx+0x28] ; real device id
uintptr_t *ptr = (uintptr_t *)(arg2 + 0x28);
// NSLog(@"ptr: %#lx", ptr);
void * addressPtr = (void *) *ptr;
NSString * deviceId = [MemoryUtils readStringAtAddress:(addressPtr+0x20)];
NSLog(@"deviceId: %@", deviceId);
_rbx.deviceID =deviceId;
}
return sub_100059E70Ori(arg0,arg1,arg2,arg3,arg4);
}
然后继续 build, dylib 注入, 启动~~
ouho~ 成功拿下, 你妈再也不用担心你买不起 APP 了
分析 arm
弟弟起的比我早, 可能在催我搞 arm 版了。
过程不多说了, 与 x86 一样 , 关键函数如下:
arm 的 bl 约等于 x86 的 call, x0寄存器 约等于 x86 的 rax.
arm 版 hook 的函数分别为 sub_100131360, sub_100050ea0
0000000100131ec8 bl sub_100131360 ; sub_100131360, CODE XREF=sub_100131978+1512
0000000100131ecc mov x21, x0
...
0000000100131f14 str x21, [x8, #0xd70] ; qword_10086ad70
0000000100217134 bl sub_100050ea0 ; sub_100050ea0
0000000100217138 mov x23, x0
...
0000000100217154 tbz w23, 0x0, loc_1002172dc
后记
01-18:
把论坛大佬写的特征码工具, 集成进来了,
@QiuChenly
https://github.com/QiuChenlyOpenSource/SearchHexCodeInFile
5.8.x 版本通杀应该没问题~
俩个函数特征码如下:
arm:
FC 6F BA A9 FA 67 01 A9 F8 5F 02 A9 F6 57 03 A9 F4 4F 04 A9 FD 7B 05 A9 FD 43 01 91 FF 03 02 D1 60
F8 5F BC A9 F6 57 01 A9 F4 4F 02 A9 FD 7B 03 A9 FD C3 00 91 56 08 40 F9 36
x86:
55 48 89 E5 41 57 41 56 41 55 41 54 53 48 81 EC 98 00 00 00 48 8D 3D
55 48 89 E5 41 57 41 56 41 55 41 54 53 50 4C 8B 62 10 4D 85 E4 74
Release
项目已经打包 github,可以直接用 xcode 打开 :
https://github.com/marlkiller/dylib_dobby_hook
目录:
- libs: 项目依赖的开源 dobby 库
- release: build 后的成品
- script: 里面有个 hack.sh, 可以直接sudo sh 执行一键注入脚本
- tools: insert_dylib 开源注入工具