手把手教你破解MacOS之Bartender 3 - 逆向分析、敏捷劫持、追溯爆破、算法推算、公钥替换、注册机!

手把手教你破解MacOS系列之Bartender 3 - 逆向分析、敏捷劫持、追溯爆破、算法推算、公钥替换、注册机!

【破文作者】tree_fly/P.Y.G
【作者邮箱】[email protected]
【作者主页】itreefly.com
【破解工具】Hopper Disassembler v4; 010 Editor;
【破解平台】MacOS
【软件名称】Bartender 3.app
【原版下载】https://www.macbartender.com/
【保护方式】-
【软件简介】-
【破解声明】请勿商用;本文仅做研究所用。

一、开始

​ 大家好,我是tree_fly。今天来分析一款macOS平台的软件(这是一款管理菜单栏图标的软件 Bartender 3 v3.0.64),尝试从不同的角度慢慢切入,尽量阐述清晰,偏于操作实战,易于上手,最重要的是一起享受沉浸逆向的愉快时光。

Bartender 3 官网:www.macbartender.com/

二、收集资料

先简单分析一些基础的信息,比如注册界面及功能限制的内容

  • 采用用户名及注册码验证模式

  • 没有网络验证

  • 4周时间全功能试用

  • 试用期结束后功能失效,弹过期购买窗口

三、逆向实战

收集完一些基本资料后,启动神器Hopper V4,载入/Applications/Bartender 3.app/Contents/MacOS/Bartender 3分析。根据一些关键词如license,快速切入到注册验证相关的函数。

Hopper V4是Hopper Disassembler V4的简称,官网:https://www.hopperapp.com/ ,v4.0.8是一个特别的版本

依据经验,[Class* isLicensed]这样的Bool返回值函数是要重点关注的。

操作实战 1

说做就做,先注入到程序,让所有[Class* isLicensed]返回true,测试一下,看看好用不好用。

这次我们换个花样玩,不在Hopper内手动修改函数开头的汇编代码:

/* return true */
mov rax, 0x1
ret

采用敏捷调试方案 Frida

正常运行Bartender 3,打开iTerm(或系统的终端),输入注入代码:

~ frida-trace -m "-[*Bartender* *icense*]" "Bartender 3"

操作无误的情况下,终端会显示如下信息:

Instrumenting functions...
-[Bartender_3.AboutPreferencesController isLicensed]: Auto-generated handler at "/Users/lg/__handlers__/__Bartender_3.AboutPreferencesCo_07d0e87a.js"
-[Bartender_3.LicensePreferencesController licenseBartenderWithSender:]: Auto-generated handler at "/Users/lg/__handlers__/__Bartender_3.LicensePreferences_b42e61ac.js"
-[Bartender_3.LicensePreferencesController licenseSegmentClickedWithSender:]: Auto-generated handler at "/Users/lg/__handlers__/__Bartender_3.LicensePreferences_c4914f22.js"
-[Bartender_3.LicensePreferencesController isLicensed]: Auto-generated handler at "/Users/lg/__handlers__/__Bartender_3.LicensePreferences_aa4237d6.js"
-[Bartender_3.AppDelegate showPreferencesLicenseView:]: Auto-generated handler at "/Users/lg/__handlers__/__Bartender_3.AppDelegate_showPr_ad1fa37a.js"
-[Bartender_3.AppDelegate isLicensed]: Auto-generated handler at "/Users/lg/__handlers__/__Bartender_3.AppDelegate_isLicensed_.js"
Started tracing 6 functions. Press Ctrl+C to stop.

最后一行提示共计有6个函数被hook,并且已经在当前工作目录下创建了“__handlers __”文件夹,其中包含了对应的6个js文件。

按下Ctrl+C中断,仔细查看终端提示的信息,其中有3个isLicensed函数被击中,分别是:

-[Bartender_3.AboutPreferencesController isLicensed]
-[Bartender_3.LicensePreferencesController isLicensed]
-[Bartender_3.AppDelegate isLicensed]

接下来要做的是逐一修改对应的js文件,比如打开-[Bartender_3.AppDelegate isLicensed]对应的Bartender_3.AppDelegate_isLicensed_.js,其内容是:

{
onEnter: function (log, args, state) {
log('-[Bartender_3.AppDelegate isLicensed]');
},
onLeave: function (log, retval, state) {
}
}

修改onLeave函数内容,仅仅加入一行代码:retval.replace(1);。代码意思就是在函数运行结束时修改返回值为1(true)。如下:

{
onEnter: function (log, args, state) {
log('-[Bartender_3.AppDelegate isLicensed]');
},
onLeave: function (log, retval, state) {
retval.replace(1); /* Just add one line code. */
}
}

其他2个函数的hook代码修改同上操作,完成了所有的js文件修改后,再次输入注入代码:

~ frida-trace -m "-[*Bartender* *icense*]" "Bartender 3"

此时!测试Bartender 3,发现没有了试用提醒,没有试用倒计时,也没有注册码输入框了,活脱脱的已激活状态。恭喜你!你已经完成了操作实战 1的内容。

然而!不要高兴的太早,面前的假象容易迷惑了双眼。4周的试用时间还没有到呢,怎么知道不会有暗桩呢~

为了交点智商税,手动修改下系统时间为4周后,果不其然,功能失效了。不要泄气,至少目前知道了是该进一步分析isLicensed的伪代码了。

操作实战 2

有了操作实战 1的经验积累,接下来逐一分析isLicensed函数的细节。

挑选其中一个查看一下伪代码:

/* @class _TtC11Bartender_311AppDelegate */
-(char)isLicensed {
rax = sub_100068310();
rax = ObjectiveC._convertBoolToObjCBool(rax & 0xff);
rax = sign_extend_64(rax);
return rax;
}

切换到Hopper的汇编代码界面,双击并进入子函数sub_100068310, 右键点击汇编代码第一行,选择References to 0x100068310查看交叉引用信息。

细心的你是否发现除了之前打交道的3个isLicensed函数,还有3处也在调用注册验证函数sub_10068310。所以明白了单靠操作实战 1为什么不会有好下场了吧。

好了,继续阅读sub_10068310的伪代码吧。为了逻辑看上去更清晰,以下展示的代码修剪了一些干扰。

int sub_100068310() {
r13 = Swift.String() -> __C.NSString(0x8000000000000000 | "license2HoldersName", 0x13);
rbx = Swift.String() -> __C.NSString(0xe800000000000000, 0x3265736e6563696c);
r13 = sub_100068b50(var_30, r14, r15, r12, 0xea00000000003272);
rbx = 0x1;
if ((r13 & 0x1) == 0x0) {
rbx = sub_100068f30(var_30, r15, r12, r14);
}
rax = rbx;
return rax;
}

0x3265736e6563696c转为字符串是2esnecil,字节高低转位就是license2

代码在读取配置文件,使用AppDelete快速查看配置文件的路径。

打开配置文件com.surteesstudios.Bartender.plist,这里记录了很多信息,但是对分析没有什么帮助,都不是关键点。

关键的是代码中的2个函数:sub_100068b50sub_100068f30,从逻辑上看任一函数返回true都能完成验证。

简单看了下两个函数的伪代码,都很长,很长,很长,真是眼花缭乱,有没有什么分析技巧呢?额,反复调试、反复分析代码就是了。

静态分析

静态分析,是相对于动态调试而言。对于新手,可以尝试倒序阅读伪代码,更容易理清代码的逻辑关系。比如sub_10068b50,仅有1个return,其结尾的代码是:

            }
}
else {
rbx = 0x0;
}
rax = rbx;
return rax;
}

这里的else提示false,继续向上看关注rbx寄存器值:

               if (r13 != 0x0) {
rbx = sub_1000627c0(r12, r14, r15, var_50, r13);
}
else {
rbx = 0x0;
}
}
}
else {
rbx = 0x0;
}
rax = rbx;
return rax;
}

所以sub_1000627c0的返回值很重要,假想返回0x1就好了。继续向上,理清逻辑关系。为了逻辑看上去更清晰,以下展示的代码修剪了一些干扰。

int sub_100068b50(int arg0, int arg1, int arg2, int arg3, int arg4) {
if ((r14 != 0x0) && (rdx != 0x0)) {
rbx = sub_100062dc0(r13, rbx, rdx, rcx, r8);
if (rbx != 0x0) {
r13 = sub_1000627c0(var_38, var_40, var_58, var_30, rbx);
if ((r13 & 0x1) != 0x0) {
rbx = 0x1;
}
}
else {
if (r13 != 0x0) {
rbx = sub_1000627c0(r12, r14, r15, var_50, r13);
}
else {
rbx = 0x0;
}
}
}
else {
rbx = 0x0;
}
rax = rbx;
return rax;
}

纵览这段代码,只要sub_1000627c0返回0x1,一切就都好了。如果进一步细看sub_1000627c0,其实已经无限接近注册码算法了,算法后面我们再分析,气氛似乎看起来有些微妙了。

deep layer func call layer
4 注册码算法层
3 sub_1000627c0
2 sub_10068b50
1 isLicensed

经过操作实战 2的分析,已经从isLicesed的第一层,逐步分析到了第三层了。恭喜你,少年!练成九阴真经指日可待!

问题来了,怎样让sub_1000627c0返回true呢?

操作实战 3

经过操作实战 2的分析,接下来要注入函数sub_1000627c0并返回true。继续采用敏捷调试方案 Frida

正常运行Bartender 3,打开iTerm(或系统的终端),输入注入代码:

~ frida-trace -i "sub_1000627c0" "Bartender 3"

提示注入失败,看来参数-i对于无符号名的函数不起作用。

再仔细阅读一下注释内容,好像也没有其他的选项可供使用:

~ frida-trace -h
Usage: frida-trace [options] target

Options:
--version show program's version number and exit
-h, --help show this help message and exit
-I MODULE, --include-module=MODULE
include MODULE
-X MODULE, --exclude-module=MODULE
exclude MODULE
-i FUNCTION, --include=FUNCTION
include FUNCTION
-x FUNCTION, --exclude=FUNCTION
exclude FUNCTION
-a MODULE!OFFSET, --add=MODULE!OFFSET
add MODULE!OFFSET
-T, --include-imports
include program's imports
-t MODULE, --include-module-imports=MODULE
include MODULE imports
-m OBJC_METHOD, --include-objc-method=OBJC_METHOD
include OBJC_METHOD
-M OBJC_METHOD, --exclude-objc-method=OBJC_METHOD
exclude OBJC_METHOD
-s DEBUG_SYMBOL, --include-debug-symbol=DEBUG_SYMBOL
include DEBUG_SYMBOL
-q, --quiet do not format output messages
-o OUTPUT, --output=OUTPUT

其实sub_xxxxxxx只是Hopper这样的逆向分析软件显示的函数名,内存中就不存在这样的函数符号名,仅提示函数的偏移地址,而且开启ASLR情况下,每次加载的地址都是随机地址。说这么多就是告诉你这句代码不起作用

对于无符号的函数,怎样用Frida来hook呢?

应该要找到程序加载的基地址module address,根据地址偏移量offset,才能定位到内存中的真实地址target address

所以,需要一些编程,但不是那么复杂,一起来看一下。

注入器的代码 inject.py
import frida
import sys
import codecs

def on_message(message, data):
print("[{}] => {}".format(message, data))

def main(target_process):
session = frida.attach(target_process)

with codecs.open(sys.argv[2], 'r', 'utf-8') as f:
source = f.read()

script = session.create_script(source)
script.on("message", on_message)
script.load()
print("[!] Ctrl+D or Ctrl+Z to detach from instrumented program.\n\n")
sys.stdin.read()
session.detach()


if __name__ == "__main__":
main(sys.argv[1])
需要注入的代码 Bartender3.js
function get_rva(module, offset) {
var base_addr = Module.findBaseAddress(module);
if (base_addr == null)
base_addr = enum_to_find_module(module);
console.log(module + ' addr: ' + base_addr);
var target_addr = base_addr.add(offset);

return target_addr;
}

var target_addr = get_rva("Bartender 3", 0x627c0);
console.log("sub_1000627c0 addr: " + target_addr);

Interceptor.attach(target_addr, {
onEnter: function(args) {
},
onLeave: function(retval) {
console.log("sub_1000627c0 return:" + retval + " replaced: 0x1");
retval.replace(0x1);
},
});

接下来,验证奇迹的时刻到了。

正常运行Bartender 3,打开iTerm(或系统的终端),输入以下命令:

~ python3 inject.py "Bartender 3" Bartender3.js

Surprise!!! 试用提示消失了,软件为已激活状态,再调整系统时间至4周后,软件依然提示激活状态,并且功能测试正常,这次不是伪破解,是完美爆破!

所以经过以上Frida的调试,最佳的Patch的方案就是修改sub_1000627c0函数开头的汇编代码,多么熟悉的味道。

/* return true */
mov rax, 0x1
ret

恭喜你,少年!你已完成操作实战 3的所有项目!并且学会了Frida的其中两种Hooking Functions方法。是时候进入下一层了。

操作实战 4

还有什么比攻破软件注册码验证机制并且写出注册机更兴奋的呢?来吧,少年,攻与防的比赛还没有结束,未来属于你的,继续读下去。

是时候分析sub_1000627c0了。还是那么长、那么长、那么长的代码,硬着头皮上吧,负责任的告诉你很快一些特殊的字符串会出现在你的眼前。

动态调试

是时候好好展示你的动态调试技术了,提前关闭已经运行的Bartender 3,打开Hopper调试器,清除所有的断点,点击运行按钮(第一个小图标),稍等片刻。

程序运行后,打开注册码输入框,输入用户名:tree_fly,以及任意密码:AAAABBBBCCCCDDDDEEEE。在点击注册按钮之前,在sub_100627c0函数头下个断点,点击注册按钮后程序断在sub_100627c0

继续按下F6,或者上图的第5个小图标,逐行Step Over运行汇编代码。

很快第一个特殊字符串SecDecodeTransformCreate出现了:

SecDecodeTransformCreate(**_kSecBase32Encoding, &var_60);

关于Security Transforms API可以参考官方文档: Security Transforms Programming Guide-Signing and Verifying

看上去是进行Base32解密,莫不会是解密注册码?快速向下定位到属性配置API看看:SecTransformSetAttribute

0000000100062919 call imp___stubs__SecTransformSetAttribute这一行下断点,直接点击运行按钮,将RIP指向此行。

000000010006290b  lea  rcx, qword [rbp+var_60]     ; argument "error" for SecTransformSetAttribute
000000010006290f mov rdi, qword [rbp+var_48] ; argument "transformRef" for SecTransformSetAttribute
0000000100062913 mov rsi, r14 ; argument "key" for SecTransformSetAttribute
0000000100062916 mov rdx, rbx ; argument "value" for SecTransformSetAttribute
0000000100062919 call imp___stubs__SecTransformSetAttribute ; SecTransformSetAttribute

如果你不是很熟悉MacOS x64的寄存器传值规则,细心的Hopper已经帮你注释出来了。

没错,\$rbx存储着即将Base32解密的value,看下就知道,切换到Debugger Console标签,输入以下调试命令:

po $rdx
<41414141 42424242 43434343 44444444 45454545>

x $rdx
0x1019a2010: 81 49 28 8e ff ff 1d 02 14 00 00 00 00 00 00 00 .I(.............
0x1019a2020: 41 41 41 41 42 42 42 42 43 43 43 43 44 44 44 44 AAAABBBBCCCCDDDD

x -c0x40 $rdx
0x1019a2010: 81 49 28 8e ff ff 1d 02 14 00 00 00 00 00 00 00 .I(.............
0x1019a2020: 41 41 41 41 42 42 42 42 43 43 43 43 44 44 44 44 AAAABBBBCCCCDDDD
0x1019a2030: 45 45 45 45 00 00 00 00 00 00 00 00 00 00 00 00 EEEE............
0x1019a2040: 31 00 00 00 00 00 00 00 1d 00 00 00 00 00 00 00 1...............

在lldb中,x是 memorry read的缩写,-c 是显示字节长度,默认是0x20。

以上读取到的正是刚才输入的注册码,看来是对注册码进行Base32解密

为了向正确的道路越走越近,伪造一串Base32加密字符串作为注册码:

Base32EncString("hello, tree_fly") = NBSWY3DPFQQHI4TFMVPWM3DZ

重新来过,这次注册码的输入框内容输入新的加密字符串,激活断点,继续向下。

很快一段奇怪的代码出现了,只见加解密执行API:SecTransformExecute,未见加解密属性配置API:SecTransformSetAttribute,以及具体的加解密方案。

    rax = sub_100063140(var_70, r14, rdx, var_50, r13, r15, rbx);
if (0x0 == 0x0) goto loc_100062b41;

loc_100062cc8:
rax = rbx;
return rax;

loc_100062b41:
rax = SecTransformExecute(rdi, &var_60);
r15 = rax;
if (r15 == 0x0) goto loc_100062cd9;

仔细查看代码,这个7个参数的子函数sub_100063140要进入看一看。

进入sub_100063140后,很快看到了 SecVerifyTransformCreate_kSecDigestSHA1,到这里一切都明朗了。

咳咳咳,敲黑板,聪明的你,看到这些信息,脑海里是不是已经浮现了具体的加解密算法。

没错,就是RSA的数字签名算法(Sign&Verify)。

既然考虑RSA算法,两个问题思考下:

  • 待加密字符串是什么及采用哪种hash方式?
  • 公布的公钥是什么?(为什么不提私钥,想什么呢,少年。)
待加密的字符串及加密方式

快速定位到 SecVerifyTransformCreate 其后的第一个SecTransformSetAttribute,在000000010006320c call imp___stubs__SecTransformSetAttribute这一行下断点,继续运行程序,成功断在此行。

00000001000631fc         mov        rbx, rax
00000001000631ff lea rcx, qword [rbp+var_28] ; argument "error" for SecTransformSetAttribute
0000000100063203 mov rdi, r14 ; argument "transformRef" for SecTransformSetAttribute
0000000100063206 mov rsi, r13 ; argument "key" for SecTransformSetAttribute
0000000100063209 mov rdx, rbx ; argument "value" for SecTransformSetAttribute
000000010006320c call imp___stubs__SecTransformSetAttribute ; SecTransformSetAttribute

输入调试代码:

po $rdx
<42617274 656e6465 72322c74 7265655f 666c79>

x -c0x40 $rdx
0x108fc27c0: 81 49 28 8e ff ff 1d 02 13 00 00 00 00 00 00 00 .I(.............
0x108fc27d0: 42 61 72 74 65 6e 64 65 72 32 2c 74 72 65 65 5f Bartender2,tree_
0x108fc27e0: 66 6c 79 00 00 00 00 00 00 00 00 00 00 00 00 00 fly.............
0x108fc27f0: ff ff ff ff 00 00 01 00 00 00 00 00 00 00 00 00 ................

所以待加密的字符串是:“Bartender2,tree_fly”,正是字符串“Bartender2,”与用户名拼接。

在反复的调试过程中,容易发现这个加密字符串还是有变化的。

x -c0x40 $rdx
0x100ebef30: 81 49 28 8e ff ff 1d 02 1a 00 00 00 00 00 00 00 .I(.............
0x100ebef40: 42 61 72 74 65 6e 64 65 72 32 55 70 67 72 61 64 Bartender2Upgrad
0x100ebef50: 65 2c 74 72 65 65 5f 66 6c 79 00 00 00 00 00 00 e,tree_fly......
0x100ebef60: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................

一个新的值出现:“Bartender2Upgrade,tree_fly”。

这也解释了操作实战 2中的那句话。

关键的2个函数:sub_100068b50sub_100068f30,从逻辑上看任一函数返回true都能完成验证。

所以,从软件作者设计注册码的角度看,其实分两类,但是,这不重要了,任选其一即可。

关于文本的hash方式,已经很明显了,就是SHA1。

公布的公钥

公钥的藏身之处要多得去了,有的存储为文件,有的以原文PEM字符串形式存储在软件中,有的还要进行解密才能获取到真正的公钥。

搜关键词SecItemImport,在下面这行下断点:

; ================ B E G I N N I N G   O F   P R O C E D U R E ================
; Variables:
; outItems: void *, 16
; importKeychain: int, 8

imp___stubs__SecItemImport: // SecItemImport
00000001000a62e8 jmp qword [_SecItemImport_ptr]

断下后,按照调用栈逆向寻找,定位到以下位置:

0000000100062f29  lea  rdx, qword [rbp+var_50]     ; argument "inputFormat" for SecItemImport
0000000100062f2d lea rcx, qword [rbp+var_48] ; argument "itemType" for SecItemImport
0000000100062f31 lea r9, qword [rbp+var_B8] ; argument "keyParams" for SecItemImport
0000000100062f38 mov esi, 0x0 ; argument "fileNameOrExtension" for SecItemImport
0000000100062f3d xor r8d, r8d ; argument "flags" for SecItemImport
0000000100062f40 mov rdi, r13 ; argument "importedData" for SecItemImport
0000000100062f43 push rax ; argument "outItems" for SecItemImport
0000000100062f44 push 0x0 ; argument "importKeychain" for SecItemImport
0000000100062f46 call imp___stubs__SecItemImport ; SecItemImport

参考Hopper的函数参数注释,importedData对应的\$rdi存储的是公钥数据,打印一下:

po $r13
<2d2d2d2d 2d424547 494e2050 55424c49 43204b45 592d2d2d 2d2d0a4d 4948774d 49476f42 67637168 6b6a4f4f 4151424d 49476341 6b45416b 61346f73 3865494d 59375469 6d58696e 51673136 5453754e 456c794d 70744c4e 6b6a680a 70474363 51763331 56446d73 36637630 52397248 6d2f4c69 4c624741 6c495146 36624c55 4f48706f 556a5333 56743947 6b514956 414d394a 7353556c 455a4461 0a415562 43467649 50706c52 3266314e 74416b41 5249652f 35415242 41583252 6676715a 52753465 37737556 65714f64 58614534 4e787064 512b2b48 57473246 790a3035 614d4e56 48367351 55755962 6133725a 78695472 3079716d 31565946 45354450 5a5a3849 36444130 4d41416b 42304b67 7245394a 6a47656c 34653566 58630a7a 6e6c7269 77314f38 48422b2b 316b476a 32793632 635a336b 76663669 76654132 6e5a786b 4e533956 4c443775 30393154 5a737464 4854456f 6b393577 4177430a 64674e7a 0a2d2d2d 2d2d454e 44205055 424c4943 204b4559 2d2d2d2d 2d0a>

x -c0x200 $r13
0x101998270: 31a3df8dffff1d008414000001000000 1...............
0x101998280: 7e010000000000007e01000000000000 ~.......~.......
0x101998290: 00000000000000000000000000000000 ................
0x1019982a0: 2d2d2d2d2d424547494e205055424c49 -----BEGIN PUBLI
0x1019982b0: 43204b45592d2d2d2d2d0a4d4948774d C KEY-----.MIHwM
0x1019982c0: 49476f42676371686b6a4f4f4151424d IGoBgcqhkjOOAQBM
0x1019982d0: 494763416b45416b61346f733865494d IGcAkEAka4os8eIM
0x1019982e0: 593754696d58696e516731365453754e Y7TimXinQg16TSuN
0x1019982f0: 456c794d70744c4e6b6a680a70474363 ElyMptLNkjh.pGCc
0x101998300: 5176333156446d733663763052397248 Qv31VDms6cv0R9rH
0x101998310: 6d2f4c694c6247416c49514636624c55 m/LiLbGAlIQF6bLU
0x101998320: 4f48706f556a5333567439476b514956 OHpoUjS3Vt9GkQIV
0x101998330: 414d394a7353556c455a44610a415562 AM9JsSUlEZDa.AUb
0x101998340: 4346764950706c523266314e74416b41 CFvIPplR2f1NtAkA
0x101998350: 5249652f35415242415832526676715a RIe/5ARBAX2RfvqZ
0x101998360: 527534653773755665714f6458614534 Ru4e7suVeqOdXaE4
0x101998370: 4e787064512b2b4857473246790a3035 NxpdQ++HWG2Fy.05
0x101998380: 614d4e5648367351557559626133725a aMNVH6sQUuYba3rZ
0x101998390: 786954723079716d3156594645354450 xiTr0yqm1VYFE5DP
0x1019983a0: 5a5a3849364441304d41416b42304b67 ZZ8I6DA0MAAkB0Kg
0x1019983b0: 7245394a6a47656c3465356658630a7a rE9JjGel4e5fXc.z
0x1019983c0: 6e6c726977314f3848422b2b316b476a nlriw1O8HB++1kGj
0x1019983d0: 32793632635a336b7666366976654132 2y62cZ3kvf6iveA2
0x1019983e0: 6e5a786b4e5339564c44377530393154 nZxkNS9VLD7u091T
0x1019983f0: 5a7374644854456f6b3935774177430a ZstdHTEok95wAwC.
0x101998400: 64674e7a0a2d2d2d2d2d454e44205055 dgNz.-----END PU
0x101998410: 424c4943204b45592d2d2d2d2d0a0000 BLIC KEY-----...

调试到这里,已经从内存中找到了软件的公钥,如何存储的呢?使用字符串“BEGIN PUBLIC”搜索看看,是不是以原文字符串存储在软件中。

果真如此,那么Patch公钥不在话下了。

通过操作实战 4,我们分析出了RSA数字签名算法,被加密的文本,加密文本的hash方式SHA1,及公钥内容。有了这些信息,离注册机还远吗?

实战操作 5

来到这里的都是好汉了,少年,你很棒!

分析公钥长度

从公布的PEM格式的公钥,谁能看出来对应私钥长度是多少位的请举手。

-----BEGIN PUBLIC KEY-----
MIHwMIGoBgcqhkjOOAQBMIGcAkEAka4os8eIMY7TimXinQg16TSuNElyMptLNkjh
pGCcQv31VDms6cv0R9rHm/LiLbGAlIQF6bLUOHpoUjS3Vt9GkQIVAM9JsSUlEZDa
AUbCFvIPplR2f1NtAkARIe/5ARBAX2RfvqZRu4e7suVeqOdXaE4NxpdQ++HWG2Fy
05aMNVH6sQUuYba3rZxiTr0yqm1VYFE5DPZZ8I6DA0MAAkB0KgrE9JjGel4e5fXc
znlriw1O8HB++1kGj2y62cZ3kvf6iveA2nZxkNS9VLD7u091TZstdHTEok95wAwC
dgNz
-----END PUBLIC KEY-----

从经验来看,推测其长度介于1024-2048之间。起初我测试了一下用1024位私钥对应的公钥Patch掉作者的公钥,发现有长度检测,注册失败。所以制造一个长度相同的公钥迫在眉睫。

等等,为什么要Patch公钥呢?看张图吧,看图说话。

继续回到密钥长度问题,首先我尝试用openssl来分析,但是提示错误信息:

~ openssl rsa -pubin -text -in public.pem
4415460972:error:06FFF07F:digital envelope routines:CRYPTO_internal:expecting an rsa key:/BuildRoot/Library/Caches/com.apple.xbs/Sources/libressl/libressl-22.250.1/libressl-2.6/crypto/evp/p_lib.c:295:

然后使用以下2个命令,指定密钥长度位于 1024-2048之间,采用二分法反复验证,以期公钥的长度相同。

openssl genrsa -out pri.pem 1024
openssl rsa -in pri.pem -pubout > pub1024.pem

最终发现长度在1678时恰如其分(不是密钥长度的唯一值)。少年,长度都帮你算好了,创建自己的密钥吧。

openssl genrsa -out pri.1678.pem 1678
openssl rsa -in pri.1678.pem -pubout > pub.1678.pem

如何手动Patch公钥

有很多的字节编辑软件,推荐010 Editor,选中字符串后原位贴入即可。16进制字节粘贴的方法选择Edit->Paste From->Paste from Hex Text。注意仔细检查替换的正确性。

用Swift 写注册机

论坛有Go写的RSA算法注册机,这里换Swift 5。

mkdir Bartender3KeyMaker
cd Bartender3KeyMaker
swift package init --type executable

以上创建了Bartender3KeyMaker文件夹以及同名的Swift项目。

打开Package.swift,添加2个dependencies:

// swift-tools-version:5.1
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
name: "Bartender3KeyMaker",
dependencies: [
.package(url: "https://github.com/IBM-Swift/BlueRSA", from: "1.0.0"),
.package(url: "https://github.com/norio-nomura/Base32", from: "0.5.4"),
],
targets: [

.target(
name: "Bartender3KeyMaker",
dependencies: ["CryptorRSA", "Base32"]),
.testTarget(
name: "Bartender3KeyMakerTests",
dependencies: ["Bartender3KeyMaker"]),
]
)

修改main.swift:

import CryptorRSA
import Base32

if #available(OSX 10.12, *) {

print("*** Bartender 3 KeyMaker ***")

let publicKeyPEM = """
-----BEGIN PUBLIC KEY-----
MIHwMA0GCSqGSIb3DQEBAQUAA4HeADCB2gKB0i4NHp5du5mNFuKif70Ra4Au7d3s
5id3pgD5X7IO6oRtDvIqWYVFON2iY01T48hUxN8BCHpbt575PAhT0cV2mUeeElNW
QxhhXo2VcP98wlbzvTM4+jnwytK7kqNQINjyuxJucm9/Ak7VuIrZpvAR72UHN2dz
FGKEie4liTy4u7/rYAqlWTjp5GvPgkk9Fspdisjm8MSxpv8q+bO6cY3sUXfN8lHI
t3HLOOuyEYnhBJ2429xrtveKEAogxagLexmucAyo3J7CYR/D6sPwjJF/SoHykwID
AQAB
-----END PUBLIC KEY-----
"""

let privateKeyPEM = """
-----BEGIN RSA PRIVATE KEY-----
MIIDyQIBAAKB0i4NHp5du5mNFuKif70Ra4Au7d3s5id3pgD5X7IO6oRtDvIqWYVF
ON2iY01T48hUxN8BCHpbt575PAhT0cV2mUeeElNWQxhhXo2VcP98wlbzvTM4+jnw
ytK7kqNQINjyuxJucm9/Ak7VuIrZpvAR72UHN2dzFGKEie4liTy4u7/rYAqlWTjp
5GvPgkk9Fspdisjm8MSxpv8q+bO6cY3sUXfN8lHIt3HLOOuyEYnhBJ2429xrtveK
EAogxagLexmucAyo3J7CYR/D6sPwjJF/SoHykwIDAQABAoHSEeoqqiL+swpvB7V9
ifi34ELhaD8bfekO3Dwm3SbuVpvyf4S4FJ9MMvRUOyXSbAGGINbPDIKXmTGOCBNL
fMzZbkHxERhyu45NcTjcn5dSJu9lAAM/XMDutjIgJoYqcRtkaRQsUnGPXUnIztPZ
S8mTiyUkOUwAyjHn5XwEam5mDj42gkrbIZk/S0THnQOdrbSZnnwcq8jmvS3g0xup
ryjOhKwe2174khAP2bD1emEM5UyygJEcsJV91NTSlqKrPbVPre5DdibGX73aWOCw
6GMv2l9RAml4eKeKZ9DIr98IJbXBIDjQaTM1HxT3ekAyWm+eiBo0qXsfvZCEOJkS
KgTzkiQ1GSh9y/N9qmixgCLKiqqsli4vHnMgwsOMnO7Lf913ELFvA7/DgOpLTOTB
lu3KLaXH+lnx6Obvsdk+6ysCaWHbutRcHcezwrQ6cO77r9IYp9xf8J6RRMqXwOGp
Gn1Qy2i5pV5KYm9wL1Mmkxr+zymTeJ964EuItPl2a/NMx7ln0UQNBU7oGG5rK1Sc
xUYWjWJGPyXA/eF80UFDryM/5nG+nA1Nb1vCOQJpFbYQ47Wv/+sKM+qv5d1Lv+ul
qeYvHiavGSQJR7XZmzIMGX1NZTbaB1cBS3BEDDm7fWhbOoOSmKKyInR5K99o9V70
eqv/GAFUW+JwZDvi7lHrpm0+TFHQTD9KHYy6et7YhOtnaz1PHLK/AmlDf1/6oh8Y
Y/FkhvrmnEvFyqPd6X76oJCmfM3Z2N4gmd3zujlKNFx5KRQ7clv9Psx9jO6icgrL
jtvlRb1n8AnC5Mz+90w2BPj1EI6uqgOYOG4E3xcnX1q+cW2Uaq8ezTCSPDs/Ia4x
yGECaTg3UbgNnQGhmrwbrtqIt+/aVjiE7UMDCl+svCvaaHHZCY1J/aRqu6JAnURU
I5qqQ46zTzJR+i9A9jryT1S11sIxuVr5hDmKC+2JA/9ogrwbYuHnCZrCx2P53Bhm
kDd2Ypk3F7cQTVgQTA==
-----END RSA PRIVATE KEY-----
"""

let publicKey = try! CryptorRSA.createPublicKey(withPEM: publicKeyPEM)
let privateKey = try! CryptorRSA.createPrivateKey(withPEM: privateKeyPEM)

print("Please input your name: ", separator: "", terminator: "")
let userName = readLine() ?? "tree_fly"

let data = "Bartender2,\(userName)".data(using: String.Encoding.utf8)!
let plainText = CryptorRSA.createPlaintext(with: data)

let signature = try plainText.signed(with: privateKey, algorithm: .sha1)!
let verification = try plainText.verify(with: publicKey, signature: signature, algorithm: .sha1)
print("Key Verification: ", verification)

let signBase32String = signature.data.base32EncodedString

print("\nName:", userName)
print("SN:", signBase32String)

} else {
print("ERROR(OSX NEED 10.12+)")
}

然后运行程序

swift run

一切顺利情况下,即可正确计算注册码:

或者:

Name: www.chinapyg.com
SN: BR7K5KAGMS4PIM2KNJOVCLP4XYHWNL6VM7C6XF2DJYZFJPR7CBJ5YUCOQBMQHDFXN6EMI6YT4P4VZ4XL543G733FXYGNRDGKKHPPXKZG2LCZX433BY7BAAQNVJZ54GBNJS6DG5ASX5D3A6LQ24FBKX5NX2A4XOR2NLMY32FOL7PVDN6GZKAAUPBTX3Z65TYBEPMYW76T4XDTPUONUNLXAUNMTPVT37G73C5HNX2QUVC5XOQCUCTTJVXDINP4NYP73FAQTBGWDNZY6LREV6QUKIA3ZYLI26NXR5XLPA4VGBNYK7Z36SXPTUHGUHSRQTMAGGAKJRFX4UNKEMPV

四、结束语

功与防的较量会一直存在,知彼知己者,百战不殆。本文可能有些地方写的不对,语句颇有累赘,旨在拓宽思路,抛砖引玉,希望能给读者带来一些启发和实战操作的引导。再次感谢您的阅读,欢迎留言,促进交流。

tree_fly/P.Y.G

2019-09-18


参考资料

~ END ~