0x00
Typora是一款强大的markdown编辑器,它可以让你轻松地写出美观的文档。但是其一直是不开源的,而且现在也已经开始收费了。所以本着学习探索的精神去逆向看看~
0x01
众所周知Typora是基于electron+reacta开发的,所以先看看js代码~
然后在顺着这个O继续康康~
关键就是这个e.hasActivated = "true" == e.hasActivated 但是这样的话有个小问题就是每次进去的话那个右下角都会有一个未激活悬浮框。而且只进行到这里也没啥意思,话不多说继续弄macos的可执行文件。
0x02
直接上ida,通过刚才的js思路走下去,这个可执行文件里肯定有生成url方法。搜索一下果不其然:
然后我们看到其调用了一个getLicensePanelUrlParams方法。进去康康!
ok!直接开始hook!!打开code编写dylib!
hook其返回值,setReturnValue后然后打印一下看看。
id LicenseWindowController = NSClassFromString(@"LicenseWindowController");
[LicenseWindowController aspect_hookSelector:@selector(pageURL) withOptions:AspectPositionBefore usingBlock:^(id<AspectInfo> info){
NSInvocation *invocation = info.originalInvocation;
[invocation invoke];
__unsafe_unretained NSArray * tempResultSet;
[invocation getReturnValue:&tempResultSet];
NSLog(@"hook==pageURL-result=%@",tempResultSet);
} error:nil ];
}
嗯,失败了。url参数没变。。估计是哪里还有验证。但是找了一下调用没看到。所以换个思路。
我们看到其获取url参数时是通过LicenseManager类的成员属性获取。所以直接修改这里,我觉得比hook返回值的url更优。那么开始查找~
查找ING~
0x03
找到了读取和写入LicenseInfo的方法那么下面就好说了~
第一眼就看到一个方法,recordFilePathNew
hook住它我们看看返回值~
v3 = objc_msgSend(&OBJC_CLASS___NSKeyedArchiver, "archivedDataWithRootObject:", self->_licenseDict, v2);
v4 = objc_retainAutoreleasedReturnValue(v3);
v5 = v4;
v6 = +[Crypto encryptAES:](&OBJC_CLASS___Crypto, "encryptAES:", v4);
v7 = (void *)objc_retainAutoreleasedReturnValue(v6);
objc_release(v5);
v8 = -[LicenseManager recordFilePathNew](self, "recordFilePathNew");
v9 = objc_retainAutoreleasedReturnValue(v8);
objc_msgSend(v7, "writeToFile:atomically:", v9, 1LL);
//writeLicenseInfo这里我们看到他是把licenseDict AES加密存到了刚才的地址中去。
//那么licenseDict里都有什么呢?
//通过 getLicensePanelUrlParams:
objc_msgSend((void *)self->_licenseDict, "objectForKeyedSubscript:", CFSTR("email"));
objc_msgSend((void *)self->_licenseDict, "objectForKeyedSubscript:", CFSTR("license"));
v27 = -[LicenseManager dayRemains](self, "dayRemains");
v28 = objc_msgSend(&OBJC_CLASS___NSString, "stringWithFormat:", CFSTR("%ld"), v27);
v29 = objc_retainAutoreleasedReturnValue(v28);
v30 = v29;
v31 = objc_msgSend(&OBJC_CLASS___NSURLQueryItem, "queryItemWithName:value:", CFSTR("dayRemains"), v29);
//我们看到了里面有email和license
//然后我们进入LicenseManager dayRemains
signed __int64 __cdecl -[LicenseManager dayRemains](LicenseManager *self, SEL a2)
{
return (signed __int64)-[LicenseManager dayRemainsFrom:](self, "dayRemainsFrom:", 15LL);
}
//通过LicenseManager dayRemainsFrom
objc_msgSend((void *)self->_licenseDict, "setObject:forKeyedSubscript:", v8, CFSTR("installDate"));
//也就是说LicenseManager类的成员字典里_licenseDict包含了邮箱和license以及installDate
installDate安装时间也就是判断我们15天试用的关键。
所以这里只要把这个AES加密后储存的文件定期删除就可以永续15天试用进行一个破解。
但是这样也不太优雅~
可是直接修改类成员hasActivated也不行。
0x04
我们先回顾一下线索。
- electron是通过接收解析url参数“hasActivated”来判断是不是激活用户。
- macos里可执行文件修改让其发送url时把hasActivated赋值为true
- getLicensePanelUrlParams 和 pageUrl修改返回值无效。
- LicenseManager类里包含两个成员。1_hasLicense,2_licenseDict
- 直接hook赋值hasActivated也不行,怀疑需要licenseDict。
- licenseDict包含邮箱和license以及installDate,可以通过修改installDate来破解试用
- licenseDict会加密储存在本地。
思考:
直接修改发送前的url和hasLicense不行,我怀疑是哪里还有验证,也就是验证licenseDict,尤其是licenseDict里的license。可以选择做掉验证license的方法(最轻松),也可以逆向算法追出正确的license(应该可以毕竟有离线验证)
但是,程序不会哪个环节都会验证license。
划重点,加密存储。既然加密了,估计程序应该会很信任这段数据吧~
所以一般是在存储前验证下license!
思路有了!
我们看看hook下writeLicenseInfo~~
上代码~
id LicenseManager = NSClassFromString(@"LicenseManager");
[LicenseManager aspect_hookSelector:@selector(writeLicenseInfo) withOptions:AspectPositionBefore usingBlock:^(id<AspectInfo> info){
NSInvocation *invocation = info.originalInvocation;
[invocation invoke];
id instance = info.instance;
[instance setValue:@"1" forKeyPath:@"hasLicense"];
NSDictionary *licenseDict = @{@"email":@"huajia@huaji.com", @"license":@"hackBy:huajia"};
[instance setValue:licenseDict forKeyPath:@"licenseDict"];
} error:nil ];
ok,我们在在原方法调用前执行我们的hook,先是修改了LicenseManager的成员属性hasLicense,让程序误认为我们已经激活,顺便把邮箱和License也加入了,哈哈。然后writeLicenseInfo会把我们的构造加密写入本地文件中,然后其他方法会读取,现在我们运行下程序看看!
233~