Dash_crack -- OSX M1(Apple Sillcon) 逆向工程实践

坚决抵制盗版行为,本文仅作 OSX 软件逆向分析及 OSX 软件安全相关的研究目的。


背景

最近在 osx 上找文档工具,想起之前在 linux 上用过一款名为 Zeal 的开源软件,顺藤摸瓜下载了 Dash,使用体验还不错。

工作环境

  • macOS Big Sur 11.5.2 (M1)
  • Dash 6.0.7 (980)
  • IDA Pro (7.6)

定位激活函数

  1. 打开软件尝试注册,发现扩展名未知。根据提示说明可以双击证书文件激活,故可以根据 info.plist 的文件关联信息获取到证书的后缀名为 dash-license.

    获取后缀
  2. 构建一空白文件,再次尝试,提示 The license file does not seem to be valid.

  3. 在应用目录下找到 Dash 二进制文件,拖入 IDA Pro,查找错误提示。

错误提示
  1. 通过查找交叉引用,定位到成员函数 -[DHInApp processLicenseFile:]
定位目标函数
  1. 速览目标函数,其内部的字符串信息表明该函数与激活存在直接关系。
确定激活函数

由于 Dash 是一个通用二进制文件(即包含 armx86_64)。 使用 x86_64 的反编译结果通常优于 arm 的结果。 hopper 分析同理。

如非必要,在静态分析阶段应优先考虑分析 x86_64 格式。

逐层分析

通过 IDA pro 的反编译,整理出如下伪代码,仅包含需要关注的部分。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
if DHInApp::displayString() == "com.barebones.textwrangler":
return ok_msg("The full version of Dash is already active. Thank you!")

# 1. 判断文件是否存在
if Not fileExistsAtPath('~/Library/Application Support/Dash/License/license.dash-license'):
cleanDir()

# 2. 拷贝文件
if Not CopyFile(curLicense, '~/Library/Application Support/Dash/License/'):
return err_msg("Dash was not able to save your license. This most likely means that you don't have write permissions in ~/Library/Application Support/Dash/License. Try to delete ~/Library/Application Support/Dash/License and try again.\n\nThe error was: %@")

# 3. 重新校验
DHInApp::setThirdB(0L)
DHInApp::setLoadingString(0L)

if DHInApp::displayString() != "com.barebones.textwrangler":
if DHInApp::upgradeAvailable():
return err_msg("This license is for Dash %ld. You need to upgrade it before you can use it with Dash 6.")
else:
return err_msg("The license file does not seem to be valid. Sorry! Please contact the developer if you need help.")

# 4.清空校验信息
DHInApp::transactionFailed()

# 5. 显示成功动画
displayViewForIdentifier::animate("Purchase")

# 6. 更新校验信息
CFDictionaryRef_sub_100058844('~/Library/Application Support/Dash/License/license.dash-license')

以上是经过整理的结果。

以上需要重点关注的部分有两个,

  • DHInApp::displayString
  • CFDictionaryRef_sub_100058844

并不能简单的让 DHInApp::displayString 返回正确的字符串,而是需要它走到下面的更新校验信息部分。

注册信息

图中的 verifier_and_loadDataFromXMLCFDictionaryRef_sub_100058844 在分析完功能后重命名的结果。


以下反编译结果均出自 IDA Pro

为了方便阅读,下文代码结果均经过重命名处理,并用 // .... 的形式省略无用代码。


深入 DHInApp::displayString

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
id __cdecl -[DHInApp displayString](DHInApp *self, SEL a2)
{
// .....
v2 = -[DHInApp loadingString](self, "loadingString");// 如果没有文件,这里第一次会返回空字符串,第二次会被设置
if ( objc_msgSend(v2, "length") )
return -[DHInApp loadingString](self, "loadingString");
v4 = -[DHInApp thirdB](self, "thirdB"); // 第一次进,这里是fail,第二次会被设置
if ( (unsigned __int8)objc_msgSend(v4, "isEqualToString:", CFSTR("fail")) )
{
// .....
return &value;
}
v6 = objc_msgSend(&OBJC_CLASS___NSFileManager, "defaultManager");
v7 = +[DHInApp licenseFile](&OBJC_CLASS___DHInApp, "licenseFile");
if ( !(unsigned __int8)objc_msgSend(v6, "fileExistsAtPath:", v7) )
goto LABEL_14;
v8 = objc_msgSend(&OBJC_CLASS___NSBundle, "mainBundle");
v9 = (__CFString *)objc_msgSend(v8, "pathForResource:ofType:", CFSTR("invalidTest"), &value);
if ( !v9 )
v9 = &value;
if ( base64_pubkey )
{
CFStringInsert(base64_pubkey, 0LL, CFSTR("0xAD97B0E07F8C747E2D2"));
CFStringAppend(base64_pubkey, CFSTR("36DC1AB0B328ABE4F2E42E4D95AEB2"));
CFStringAppend(base64_pubkey, &value);
CFStringAppend(base64_pubkey, CFSTR("A"));
CFStringAppend(base64_pubkey, CFSTR("A"));
CFStringAppend(base64_pubkey, CFSTR("7AF4B41F4D84B0C70248A78E6497"));
CFStringAppend(base64_pubkey, CFSTR("483"));
CFStringAppend(base64_pubkey, CFSTR("C"));
CFStringAppend(base64_pubkey, CFSTR("C"));
CFStringAppend(base64_pubkey, CFSTR("9A95EC0652DA58BAEC965C106"));
CFStringAppend(base64_pubkey, CFSTR("CC3B67DD3D0694CB05"));
if ( base64_pubkey )
{
create_global_pubkey(base64_pubkey);
CFRelease(base64_pubkey);
base64_pubkey = 0LL;
}
}
if ( (unsigned __int8)checkurl_and_anti_crack(0LL) )// 这里是防破解,我们需要返回假
goto LABEL_14;
v10 = (const __CFURL *)objc_msgSend(&OBJC_CLASS___NSURL, "fileURLWithPath:", v9); // 这里使用了无效文件进行检验破解
if ( (unsigned __int8)checkurl_and_anti_crack(v10) )// 同上,这里也需要返回假
goto LABEL_14;
if ( (unsigned __int8)checkurl_and_anti_crack(0LL) )
goto LABEL_14;
v12 = +[DHInApp licenseFile](&OBJC_CLASS___DHInApp, "licenseFile");
v13 = (const __CFURL *)objc_msgSend(&OBJC_CLASS___NSURL, "fileURLWithPath:", v12);
if ( !(unsigned __int8)checkurl_and_anti_crack(v13) )// 真正的证书文件,这里需要返回真
goto LABEL_14;
if ( (unsigned __int8)checkurl_and_anti_crack(0LL) )// 同上,返回假
goto LABEL_14;
-[DHInApp setUpgradeAvailable:](self, "setUpgradeAvailable:", 0LL);
v14 = +[DHInApp licenseFile](&OBJC_CLASS___DHInApp, "licenseFile");
v15 = (const __CFURL *)objc_msgSend(&OBJC_CLASS___NSURL, "fileURLWithPath:", v14);
DataFromXML = verifier_and_loadDataFromXML(v15);
v31 = objc_autorelease(DataFromXML);
v17 = objc_msgSend(v31, "objectForKeyedSubscript:", CFSTR("Signature"));
v18 = objc_msgSend(&OBJC_CLASS___NSBundle, "mainBundle");
v19 = objc_msgSend(v18, "resourcePath");
v20 = objc_msgSend(v19, "stringByAppendingPathComponent:", CFSTR("BlockedLicenses.plist"));
v21 = objc_msgSend(&OBJC_CLASS___NSDictionary, "dictionaryWithContentsOfFile:", v20);
if ( v17 )
{
v22 = objc_msgSend(v21, "objectForKeyedSubscript:", CFSTR("licenses"));
if ( (unsigned __int8)objc_msgSend(v22, "containsObject:", v17) )
goto LABEL_14;
}
v23 = objc_msgSend(v31, "objectForKeyedSubscript:", CFSTR("Email"));
if ( (unsigned __int8)objc_msgSend(v23, "isEqualToString:", CFSTR("lizhixiangwang@gmail.com")) ) // 黑名单邮箱
{
LABEL_14:
v11 = +[DHAnalytics sharedAnalytics](&OBJC_CLASS___DHAnalytics, "sharedAnalytics");
objc_msgSend(v11, "trackAppLaunch:", 0LL);
-[DHInApp setThirdB:](self, "setThirdB:", CFSTR("fail"));
return &value;
}
v24 = objc_msgSend(v31, "objectForKeyedSubscript:", CFSTR("Version"));
v25 = objc_msgSend(v24, "integerValue");
-[DHInApp setLicenseDashVersion:](self, "setLicenseDashVersion:", v25);
v26 = -[DHInApp licenseDashVersion](self, "licenseDashVersion");
v27 = 2LL;
if ( v26 >= 3 ) // 过滤错误的版本号
v27 = -[DHInApp licenseDashVersion](self, "licenseDashVersion", 2LL);
-[DHInApp setLicenseDashVersion:](self, "setLicenseDashVersion:", v27);
if ( (__int64)-[DHInApp licenseDashVersion](self, "licenseDashVersion") < 6 )
{
-[DHInApp setUpgradeAvailable:](self, "setUpgradeAvailable:", 1LL);
if ( (unsigned __int8)objc_msgSend(&OBJC_CLASS___NSThread, "isMainThread") )
{
-[DHInApp swapUpgradeView](self, "swapUpgradeView");
}
else
{
// ....
dispatch_sync(&_dispatch_main_q, v30);
}
goto LABEL_14;
}
-[DHInApp setActivatedByLicense:](self, "setActivatedByLicense:", 1LL);
-[DHInApp setLoadingString:](self, "setLoadingString:", CFSTR("com.barebones.textwrangler"));
if ( (unsigned __int8)objc_msgSend(&OBJC_CLASS___NSThread, "isMainThread") )
{
-[DHInApp setLicensePurchaseDescription](self, "setLicensePurchaseDescription");
-[DHInApp swapGiftView](self, "swapGiftView");
}
else
{
// ....
dispatch_sync(&_dispatch_main_q, block);
}
v28 = +[DHAnalytics sharedAnalytics](&OBJC_CLASS___DHAnalytics, "sharedAnalytics");
objc_msgSend(v28, "trackAppLaunch:", 1LL);
return -[DHInApp loadingString](self, "loadingString");
}

在细说上面的结论前,先观察函数的控制流图。

Graph

这是一张有意思的图,通常这种密集的线条极有可能是一个公共的错误处理过程,这里暂称为公共出口。

结合上面的代码,该函数 return 出口共有4个。

其中第一个是激活后一定被触发的,第二是是在激活前必然触发的(可以通过对初始值分析得到)。剩下正确的出口和错误的出口各一个。

LABEL_14 属于图中的公共出口,再结合反编译的上下文看,公共出口即为错误处理。也就是说,流程上不进入 goto LABEL_14 即可完成激活。

checkurl_and_anti_crack 是完成流程的关键函数。

checkurl_and_anti_crack

又一次见到了 verifier_and_loadDataFromXML,后面会对它一探究竟。

接着看一下另外一个感兴趣的点 base64_pubkey,通过对 create_global_pubkey 函数的内容分析,基本确定该全局变量存放着公钥,内容是通过对字符串 0xAD97B0E07F8C747E2D2......转换得到。

再见 verifier_and_loadDataFromXML

1
2
3
4
5
6
7
8
9
10
11
12
CFDictionaryRef __fastcall verifier_and_loadDataFromXML(const __CFURL *a1)
{
// ...
v1 = 0LL;
if ( CFURLCreateDataAndPropertiesFromResource(kCFAllocatorDefault, a1, &resourceData, 0LL, 0LL, errorCode) == 1
&& !errorCode[0] )
{
v1 = VerifySignatureFromResourceData(resourceData);
CFRelease(resourceData);
}
return v1;
}

最终需要通过 VerifySignatureFromResourceData 返回字典给外部使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
CFDictionaryRef __fastcall VerifySignatureFromResourceData(const __CFData *a1)
{
// ......
if ( global_publickey ) // 判断公钥解密是否正常
{
errorString = 0LL;
v1 = kCFAllocatorDefault;
v2 = CFPropertyListCreateFromXMLData(kCFAllocatorDefault, a1, 1uLL, &errorString);
v32[3] = (__int64)v2;
if ( !errorString
&& (TypeID = CFDictionaryGetTypeID(), TypeID == CFGetTypeID((CFTypeRef)v32[3]))
&& CFPropertyListIsValid((CFPropertyListRef)v32[3], kCFPropertyListXMLFormat_v1_0) )
{
v4 = (const __CFDictionary *)v32[3];
Value = (const __CFData *)CFDictionaryGetValue(v4, CFSTR("Signature"));
if ( Value ) // 判断证书是否存在 Signature 字段
{
signature = Value;
sha_sourceData = (const __CFData *)use_sha_construct_sourceData(v4); // 构造原始数据
v23[3] = (__int64)sha_sourceData;
if ( sha_sourceData )
{
Length = CFDataGetLength(sha_sourceData);
BytePtr = CFDataGetBytePtr(sha_sourceData);
Mutable = CFStringCreateMutable(kCFAllocatorDefault, 2 * Length);
if ( Length > 0 )
{
v49 = v4;
v48 = kCFAllocatorDefault;
for ( i = 0LL; i != Length; ++i )
CFStringAppendFormat(Mutable, 0LL, CFSTR("%02X"), BytePtr[i]);
v1 = v48;
v4 = v49;
}
}
else
{
Mutable = CFStringCreateMutable(kCFAllocatorDefault, 0LL);
}
if ( qword_1003B0B68 )
CFRelease(qword_1003B0B68);
qword_1003B0B68 = CFStringCreateCopy(v1, Mutable);
CFRelease(Mutable);
v14 = (const __CFArray *)theArray;
if ( !theArray
|| (Count = CFArrayGetCount((CFArrayRef)theArray),
!CFArrayContainsValue(v14, (CFRange)__PAIR128__(Count, 0LL), Mutable)) )
{
v16 = SecVerifyTransformCreate(global_publickey, signature, (CFErrorRef *)v36 + 3);
v28[3] = (__int64)v16;
if ( !v36[3] )
{
SecTransformSetAttribute(v16, kSecTransformInputAttributeName, (CFTypeRef)v23[3], (CFErrorRef *)v36 + 3);
if ( !v36[3] )
{
SecTransformSetAttribute(
(SecTransformRef)v28[3],
kSecInputIsAttributeName,
kSecInputIsRaw,
(CFErrorRef *)v36 + 3);
if ( !v36[3] )
{
v17 = (const __CFBoolean *)SecTransformExecute((SecTransformRef)v28[3], (CFErrorRef *)v36 + 3); // 验证原始数据签名
v19[3] = (__int64)v17;
if ( !v36[3] && kCFBooleanTrue == v17 )
{
Copy = CFDictionaryCreateCopy(v1, v4);
v40(v39);
goto LABEL_15;
}
}
}
}
}
}
else
{
CFShow(CFSTR("No signature"));
}
v40(v39);
}
else
{
v11 = (const void *)v32[3];
if ( v11 )
CFRelease(v11);
}
}
else
{
CFShow(CFSTR("Public key is invalid"));
}
Copy = 0LL;
LABEL_15:
// ....
return Copy;
}

这里本质是一个签名验证过程,通过证书中的 Signature 及公钥验证证书本身的合法性。

RSA 签名验证

简单比划一下 RSA签名验证 过程。

1
2
3
4
5
6
7
-> : 生成
<->: 验证

私钥 -> 公钥
私钥 + 原始数据 -> 签名

公钥 <-> 原始数据 + 签名

上面代码中的 use_sha_construct_sourceData 就值得注意了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
CFTypeRef __fastcall use_sha_construct_sourceData(CFDictionaryRef theDict)
{
// .......
Count = CFDictionaryGetCount(theDict);
allocator = kCFAllocatorDefault;
theArray = CFArrayCreateMutable(kCFAllocatorDefault, Count, 0LL);
v36 = &v22;
v2 = (const void **)((char *)&v22 - ((8 * Count + 15) & 0xFFFFFFFFFFFFFFF0LL));
CFDictionaryGetKeysAndValues(theDict, v2, 0LL);
if ( Count > 0 )
{
for ( i = 0LL; i != Count; ++i )
{
if ( CFStringCompare((CFStringRef)v2[i], CFSTR("Signature"), 0LL) )// 排除 Signature
CFArrayAppendValue(theArray, v2[i]);
}
}
v5 = theArray;
CFArraySortValues(theArray, (CFRange)__PAIR128__(Count - 1, 0LL), (CFComparatorFunction)&_CFStringCompare, (void *)1);// 字典转成数组并根据字符大小排序
v6 = allocator;
theData = CFDataCreateMutable(allocator, 0LL);
v7 = CFArrayGetCount(v5);
if ( v7 > 0 )
{
v8 = (unsigned int)v7;
for ( j = 0LL; j != v8; ++j )
{
ValueAtIndex = CFArrayGetValueAtIndex(v5, j);
Value = (const __CFString *)CFDictionaryGetValue(theDict, ValueAtIndex);
ExternalRepresentation = CFStringCreateExternalRepresentation(v6, Value, 0x8000100u, 0);
BytePtr = CFDataGetBytePtr(ExternalRepresentation);
Length = CFDataGetLength(ExternalRepresentation);
v15 = BytePtr;
v5 = theArray;
CFDataAppendBytes(theData, v15, Length);
v16 = ExternalRepresentation;
v6 = allocator;
CFRelease(v16);
}
}
v17 = theData;
v18 = SecDigestTransformCreate(kSecDigestSHA1, 0LL, (CFErrorRef *)v33 + 3);
v23[3] = (__int64)v18;
if ( v33[3] )
{
CFRelease(v17);
v28();
}
else
{
SecTransformSetAttribute(v18, kSecTransformInputAttributeName, v17, (CFErrorRef *)v33 + 3); // 对数据进行 sha 取样
v19 = SecTransformExecute((SecTransformRef)v23[3], (CFErrorRef *)v33 + 3);
CFRelease(v17);
v21 = v33[3];
v28();
if ( !v21 )
goto LABEL_12; // 返回待验证的数据
if ( v19 )
CFRelease(v19);
}
v19 = 0LL;
LABEL_12:
// ....
return v19;
}

至此可以得到一个较为完整的算法过程

  1. 使用某字节转换算法将 base64_pubkey 转换,然后使用 kSecBase64Encoding 编码拼接得到公钥
  2. 把排除 Signature 的字段的值合并为数组,在 Sort 排序及其他处理后使用 SHA1 取样作为待验证数据
  3. 通过 待验证数据 pubkey Signature 完成校验

破解思路

  1. 公钥替换并实现注册机,需要反推 base64_pubkey
  2. 字节补丁,对部分流程绕过处理

虽然公钥替换可以相对避免暗桩等问题,但步骤较为麻烦。本文采用字节补丁的方式完成验证。

需要打补丁的有两处:

  1. 使 VerifySignatureFromResourceData 中的签名验证部分恒返回真
  2. 绕开 DHInApp displayString 的无效文件验证

同时根据需求字段构造文件构造一个假的 license.dash-license

1
2
3
4
5
6
7
8
9
10
11
12
13
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Email</key>
<string>piz-ewing@gmail.com</string>
<key>Version</key>
<string>6</string>
<key>Signature</key>
<data>piz-ewing
</data>
</dict>
</plist>

Patch

切换至 arm 版本,定位到关键点,打上补丁。

Activated
1
2
000000010004f7c8 CMP X8, X0                    ->  CMP X8, X8
00000001000AFE44 CBNZ W0, loc_1000AFE54 -> NOP

尝试激活,简单修改系统时间验证。

Activated

至此基本完成了Dash的破解思路验证。可能会有其他暗桩需要在实际使用中遇到后分析处理。

总结

本文主要是窥探一下 OSX软件的常用验证方法及破解思路,欢迎讨论与共同学习。

Dash 是一款很不错的软件,建议有能力的朋友可以支持正版软件发展。