用一个软件学习多姿势绕过
免责声明:
别问为什么没有成品。请各位支持正版!
本文章仅以绕过验证流程来研究软件测试和网页测试的异同点。
文章仅供技术研究交流,不得用于非法牟利并谢绝任何形式的传播。可能造成的一切法律责任由使用者、传播者承担,文章仅用于技术研究交流,其他一切责任作者概不负责。
下载软件,各位直接去官网下载就好,这边就不贴下载链接了。
初步分析
使用ida加载,在字符串中页面搜索可能的字段,如subscription
、trial
、days
、license
……
在搜索trial
时发现setTrialLength:
字段,推测这个字段跟试用期长度相关,果断双击跳转过去:
键盘输入x
进行交叉索引,就只有一处调用该字段,如图:
参考链接:
https://juejin.cn/post/7313446602442407974
跳转到位置,伪代码如下:
id sub_1001B8EE0()
{
NSString v0; // r14
NSString v1; // r15
void *InitializedObjCClass; // rax
id v3; // rax
id v4; // rbx
NSString v5; // r14
id v6; // rax
NSBundle *v7; // r13
void *object; // r12
NSString v9; // r15
Class isa; // r15
Class v11; // r15
NSString v12; // r15
id v13; // rax
id v14; // r14
NSString v15; // r15
id v16; // rax
id v17; // r12
id v19; // [rsp+0h] [rbp-30h]
Swift::String_optional v20; // 0:rdx.16
Swift::String v21; // 0:rdi.16
Swift::String v22; // 0:^8.8,8:^10.8
Swift::String v23; // 0:r9.8,8:^0.8
v0 = String._bridgeToObjectiveC()();
v1 = String._bridgeToObjectiveC()();
InitializedObjCClass = (void *)swift_getInitializedObjCClass(
&OBJC_CLASS___PADProductConfiguration,
0xE600000000000000LL);
v3 = objc_msgSend(InitializedObjCClass, "configuration:vendorName:", v0, v1);
v4 = objc_retainAutoreleasedReturnValue(v3);
objc_release(v0);
objc_release(v1);
if ( !v4 )
BUG();
v5 = String._bridgeToObjectiveC()();
objc_msgSend(v4, "setTrialText:", v5);
objc_release(v5);
v19 = (id)swift_getInitializedObjCClass(&OBJC_CLASS___NSBundle, "setTrialText:");
v6 = objc_msgSend(v19, "mainBundle");
v7 = (NSBundle *)objc_retainAutoreleasedReturnValue(v6);
v21._countAndFlagsBits = -3458764513820540889LL;
v21._object = "NSComboBoxDelegate" + 0x8000000000000000LL;
v20.value._countAndFlagsBits = 0LL;
v20.value._object = 0LL;
v23._countAndFlagsBits = 0LL;
v22._object = (void *)0xE000000000000000LL;
v22._countAndFlagsBits = 0LL;
v23._object = (void *)0xE000000000000000LL;
object = NSLocalizedString(_:tableName:bundle:value:comment:)(v21, v20, v7, v23, v22)._object;
((void (__fastcall *)(NSBundle *))objc_release)(v7);
v9 = String._bridgeToObjectiveC()();
swift_bridgeObjectRelease(object, object);
objc_msgSend(v4, "setLocalizedTrialText:", v9);
((void (__fastcall *)(NSString))objc_release)(v9);
objc_msgSend(v4, "setTrialType:", 2LL);
sub_1001B9170(0LL);
isa = NSNumber.init(integerLiteral:)(7LL).super.super.isa;
objc_msgSend(v4, "setTrialLength:", isa);
((void (__fastcall *)(Class))objc_release)(isa);
v11 = NSNumber.init(floatLiteral:)(7.99).super.super.isa;
objc_msgSend(v4, "setPrice:", v11);
((void (__fastcall *)(Class))objc_release)(v11);
v12 = String._bridgeToObjectiveC()();
objc_msgSend(v4, "setCurrency:", v12);
((void (__fastcall *)(NSString))objc_release)(v12);
v13 = objc_msgSend(v19, "mainBundle");
v14 = objc_retainAutoreleasedReturnValue(v13);
v15 = String._bridgeToObjectiveC()();
v16 = objc_msgSend(v14, "pathForImageResource:", v15);
v17 = objc_retainAutoreleasedReturnValue(v16);
((void (__fastcall *)(id))objc_release)(v14);
((void (__fastcall *)(NSString))objc_release)(v15);
objc_msgSend(v4, "setImagePath:", v17);
((void (__fastcall *)(id))objc_release)(v17);
return v4;
}
关注isa = NSNumber.init(integerLiteral:)(7LL).super.super.isa;
:
NSNumber.init(integerLiteral:)(7LL)
创建了一个值为7
的NSNumber
对象。
.super.super.isa
试图通过该对象的isa指针链向上访问其类和元类。
下边的伪代码如下
objc_msgSend(v4, "setTrialLength:", isa);
使用对象 v4
,调用它的 setTrialLength:
方法,并且传递 isa
作为参数。
此流程,初始化了7天,然后将值付给了v4
对象的setTrialLength:
方法。此时间跟我实际的试用期时长一致,big胆推测一下,此处就是我们要找到的位置,准备收工。
返回汇编界面,此处是将立即数7
放到edi
寄存器中,用于后续的计算。
使用010修改数据,将07
修改为09
尝试一下,使用010修改数据(1修改立即数/修改网页js固定量)
数据的大端和小端,为什么7的汇编是BF 07 00 00 00而不是BF 00 00 00 07
https://www.cnblogs.com/luxiaoxun/archive/2012/09/05/2671697.html
https://www.ruanyifeng.com/blog/2022/06/endianness-analysis.html
启动,修改失败……(因为到写这篇文章的是将为止,我还剩一天的试用期,所以试用期长度并没有发生变化)
二次分析
此时陷入了怀疑,它怎么可能会失败呢。
重新分析一下逻辑链条。
- 字符串中搜索到
setTrialLength:
- 这个字符串只有一处调用
- 这个值是直接set的,伪代码中有7天,与我实际的试用期时常一致。
既然这三点都成立,那么真相只有一个,这一处是个干扰选项,不出现在实际运行的代码逻辑中,不信我调试一下给你看。
打开使用lldb调试该程序
lldb命令快捷查找
https://www.dllhook.com/post/51.html
设置断点,该断点是赋值7天的位置。lldb命令如下:
b 0x1001B9062
诶诶诶,你怎么会断住的呀,这不科学,不过数字确实是我改写的9,继续执行一下
实际的9并没有生效,还是只有7天
emmm,难不成是我分析的逻辑链条错误了?
遇事不决先摆烂,万一他过两天就好了呢。
峰回路转
既然此处不留爷自有留爷处,好汉就该能曲能伸。
看看激活许可证这边能不能绕,一般这个功能点是可以设置合法证书进行校验的。
这个功能的界面是这样的。
看一下激活需要邮箱地址和许可码,而不是普通的只需要一个license,那么此处大概率是服务器校验的,抓个包看一下。
重启一下软件,诶,这t喵的是啥
emmmm,就这么简单?改改试试。(2 修改服务器返回)
成功的有点过于草率了,都准备与其大战三百回合,却发现对面已经躺平了。
柳岸花明
软件的流程现在也基本清晰了,
- 客户端走本地校验流程,设置时间为7天。
- 向服务器发送数据,接收服务器返回判断日期为7天。
- 客户端弹出试用期期限。
此时单次的验证流程已经完成了绕过,该准备一下持久化了。就跟网站测试一样,你手动过掉了网站加解密,后边每次测试都还要手动操作那也太繁琐了,该准备自动化工具了。
现在每次打开都得抓包,在使用流程中十分繁琐,先断个网,看看可不可以走客户端校验的验证逻辑。
网确实断掉了,试用期还是之前的99999993天,明显是缓存了之前的数据。
那只要让他的网络不通也就可以无限试用了。(4 缓存机制/懒加载机制,浏览器缓存)
曲境探幽
现在每次打开都得点一下继续试验,优化掉会这个流程会方便很多,尝试改一下让其不显示试用弹窗。
查看一下软件运行到0x1001B9062
的堆栈信息。
调用栈信息
https://www.dllhook.com/post/51.html#toc_10
在ida中从上到下排查,找到这个地址:0x1001B954E
此处伪代码主要如下:
id __fastcall sub_1001B9410(__int64 a1, __int64 a2)
{
……
v3 = OBJC_IVAR____TtC6Movist16PaddleController____lazy_storage___paddle;
v4 = *(void **)(v2 + OBJC_IVAR____TtC6Movist16PaddleController____lazy_storage___paddle);
if ( v4 )
{
v5 = *(id *)(v2 + OBJC_IVAR____TtC6Movist16PaddleController____lazy_storage___paddle);
}
else
{
InitializedObjCClass = swift_getInitializedObjCClass(&OBJC_CLASS___Paddle, a2);
……
}
判断在此处的下边使用了框架paddle(5 通用框架漏洞/xxx框架通用漏洞)
既然找到了框架的信息,此时肯定要查看一下是如何控制弹窗的。
找一下框架信息,查看如何出现弹窗。找到了函数showProductAccessDialogWithProduct
ida中搜索一下:
跟踪一下跳转,伪代码如下:
void __fastcall sub_1001B9E30(void *a1, void *a2)
{
objc_class *v2; // rax
id v3; // rbx
NSString v4; // r14
id v5; // rbx
__int64 v6; // rax
void *ObjCClassFromMetadata; // rax
id v8; // rax
id v9; // r14
unsigned __int64 v10; // rax
if ( (unsigned __int8)objc_msgSend(a1, "activated") )
{
v2 = (objc_class *)type metadata accessor for LicenseViewController(0LL);
v3 = objc_allocWithZone(v2);
v4 = String._bridgeToObjectiveC()();
v5 = objc_msgSend(v3, "initWithNibName:bundle:", v4, 0LL);
objc_release(v4);
v6 = type metadata accessor for ModalWindow(0LL);
ObjCClassFromMetadata = (void *)swift_getObjCClassFromMetadata(v6);
v8 = objc_msgSend(ObjCClassFromMetadata, "windowWithContentViewController:", v5);
v9 = objc_retainAutoreleasedReturnValue(v8);
v10 = (unsigned __int64)objc_msgSend(v9, "styleMask");
objc_msgSend(v9, "setStyleMask:", v10 & 0xFFFFFFFFFFFFFFF7LL);
if ( !NSApp )
BUG();
objc_msgSend(NSApp, "runModalForWindow:", v9);
objc_release(v5);
}
else
{
v9 = sub_1001B9560();
objc_msgSend(a2, "showProductAccessDialogWithProduct:", v9);
}
objc_release(v9);
}
根据代码可以判断出来如果程序已经激活,就走上边的流程,如果没激活,就走下边的流程,修改认证逻辑,让其走上边的流程。(3 修改调用分支/js动态调试控制分支)
重新启动,弹出如下界面。
可以使用了,但是感觉还是不够优雅,还有一个弹窗。那么就来点暴力的,直接给nop
掉弹窗函数。(6 暴力修改/魔改js)
查看汇编发现涉及到此处弹窗的是这里
那么直接给暴力nop
掉,令其在此处不进行任何操作。
程序已经不进行弹窗提醒。
程序可以正常使用
还剩下paddle框架还没分析,看到一篇文章Paddle激活类Mac应用通杀,后续准备根据这个大佬给的思路进行学习操作一下。
总结
软件测试流程和网站的测试流程整体相似:
根据此次软件,一共尝试了四种操作思路。
- 直接修改客户端具体的日期,尝试获取长时间的试用。(修改网页js固定量)
- 直接修改数据包响应数据,进行软件测试。(修改服务器返回)
- 直接将程序验证逻辑进行修改,使其跳转到不同分支。(js动态调试控制分支)
- 缓存机制,将之前的数据缓存在客户端(js懒加载策略,浏览器缓存)
另外还学到了两个思路
- 根据框架进行通用型修改(后续尝试)(xxx框架通用漏洞)
- 直接修改软件内容,暴力修改掉其他逻辑(
nop
操作)(魔改js)
创造性的想一下:
根据第6点,我需要对程序进行大量的nop
操作,
再根据第3点,我可以控制程序的跳转。
那么在此处,我的程序是要运行到到左分支的0x1001b9F24
,他前边的一些命令是需要忽略的,那么我是不是可以在0x1001b9f0a
处控制我的跳转长度,直接跳转到0x1001b9F24
,这样就不需要大量的进行nop
操作了。有兴趣的可以自己尝试。