吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 8343|回复: 87
上一主题 下一主题
收起左侧

[MacOS逆向] DaisyDisk v4.31 破解记录

  [复制链接]
跳转到指定楼层
楼主
0xBrian 发表于 2025-7-8 03:17 回帖奖励
本帖最后由 0xBrian 于 2025-7-9 03:02 编辑

DaisyDisk v4.31 破解

简单介绍

DaisyDisk是一款用于显示文件在磁盘中占比的软件,功能类似于Windows上的TreeSize Free,但稍微弱些,每次打开都需要选择磁盘并扫描,对于1T的盘大概需要近一分钟。软件采用一次性付费模式,通过授权码验证身份。

破解这个是因为刚开始学习逆向,随便抓了个程序过来练手,记录一下自己的工作和思考的过程

破解过程

首先打开软件,观察到功能界面闪烁了一下就被开始试用界面覆盖,

点击我有激活码会弹出一个完成激活的窗口,输入错误的激活码之后完成激活窗口会抖动

准备先用lldb得到”注册激活”这个按钮触发的函数是什么,这边直接重新签名让hardened runtime失效之后发现程序会立马退出,并且在日志输出“invalid code signature”,拖入IDA,通过字符串搜索发现程序启动时做了签名校验,先狠狠NOP掉

然后用lldb attach上去,从窗口开始找到button,再找到action,最终定位目标函数

(lldb) po [[NSApplication sharedApplication] windows]
<__NSArrayM 0x600003758270>(
<MainWindow: 0x156014a50>,
<OverlayWindow: 0x1560180e0>,
<OverlayWindow: 0x154792b90>,
<TUINSWindow: 0x1560619f0>,
<NSWindow: 0x14478d860>,
<OverlayWindow: 0x1547c7d30>
)
(lldb) po [0x14478d860 title]
注册激活 DaisyDisk
(lldb) po [(NSWindow *)0x14478d860 windowController]
<RegistrationWindowController: 0x144623660>
(lldb) po [[(NSWindow *)0x14478d860 contentView] _subtreeDescription]
...
      [   AF      w   ] h=&-- v=&-- SimpleButton 0x144761030 "注册激活" f=(381.5,10,77.5,26) b=(-) => <NSViewBackingLayer: 0x6000037ab030>
...
(lldb) po (SEL)[0x144761030 action]
"completeRegistration:"

在IDA中找到函数狠狠按下F5开读

大致梳理下逻辑

def completeRegistration():
        l1 = loadLicenseFromDisk() 
        l2 = makeLicenseHeadFromInput() 
        l3 = buildLicense(l2)
        if not l3 or isLicenseBlackListed(l3) or isLicenseCompormised(l3):
                raise Error("E")
        else:
                saveLicenseAndSetup(l3)

def buildLicense(license: Option[License]) -> Optional[License]:
        if not license:
                return None
        name = license.name
        licenseKey = license.key
        decodedLicensekey = base32_decode(licenseKey)
        valid = verify(decodedLicensekey, name, publicKey())
        if not valid:
                return None
        return _buildLicense()

这里发现程序使用的publicKey仅有55字节,结构是一个DER的RSA publicKey

先尝试算私钥,这边确实算出来了,但是由于学艺不精构造出的license key总是无法通过,遂失去耐心,准备开始爆破,思路是让buildLicense也就是sub_10010F8D8认可所有的license key,直接返回一个完整的license对象,completeRegistration这个函数没有用到从磁盘上读取的license,所以猜测程序在启动时一定会检测磁盘上的license做设置,最简单的方式就是在磁盘上先构建好一个license文件,然后等待他的设置函数帮我们设置好激活

回到mainWindow刚创建的时候主界面被覆盖的时刻,发现有一个函数 -[MainWindowController updateLicenseBehaviorAndShowNotification:]: ,仔细看发现这个应该就是我们需要的函数

然后通过查看licenseKeyFromDiskOutIsPortable函数确定license文件结构

文件是一个NSUserDefaults, 也就是plist,里面两个值一个name一个key

再找到文件路径

构造文件

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>License_CustomerName</key>
    <string>WO</string>
    <key>License_RegistrationKey</key>
    <string>WOOOOOOOOOOOOO0000oOOOOO0oOOO0OOOooO0OOO0</string>
</dict>
</plist>

patch sub_10010F8D8 让他变成这样

def buildLicense(license: Option[License]) -> Optional[License]:
        return License(
                license.name,
                licenes.key,
                0, #签发日期
                1, #版本
                9, #副本数
        )

也就是等他拿到参数之后狠狠跳到生成license的部分

最终结果伪代码如下

到这里应该就结束了,直接运行,结果弹出了这个

呜呜呜别骂了别骂了好哥哥,回头找到地方打工给你补票还不行嘛

重新进入updateLicenseBehaviorAndShowNotification开始单步调试,发现是之前比较粗,只改了sub_10010F8D8里面isLicenseCompormised的调用,忘了直接把他改了,遂patch掉这个函数

再次运行,应该是成功了,不想看他还有没有什么上传key为黑名单啥的操作了,目前的功能也够用,一个磁盘整理工具也不需要联网,所以就直接把它禁止联网了

最后

其实更优雅的解决方案可能还是通过推出来的私钥去构造任意的License,这样完全不需要侵入源文件,但是学艺不精实在是没搞出来,这里吧拿到的公钥和我推出来的私钥分享出来给有兴趣的兄弟稍微省点事儿

-----BEGIN PUBLIC KEY-----
MDUwDQYJKoZIhvcNAQEBBQADJAAwIQIaALeN7XjhVyfaLWpCJDR8Ke/6E2KcvAKI
HqMCAwEAAQ==
-----END PUBLIC KEY-----

-----BEGIN RSA PRIVATE KEY-----
MIGKAgEAAhoAt43teOFXJ9otakIkNHwp7/oTYpy8AogeowIDAQABAhkVPP/QL8qk
2tLMa7C/mWP+gQlqWLzGuM1xAg0MpeIf+HSxurX+9AgpAg0OgzYo5Qcyt7fph7nr
Ag0IjDc1ChG7Z4bF5iB5Ag0Ft3omMdHcELLJs0VxAg0BhvFKthFCuBMpyM1d
-----END RSA PRIVATE KEY-----

最后的最后

这是我第二次尝试破解应用,第一次是一个iOS App,感觉还是手忙脚乱,很多基础知识都不知道,全靠AI吊着,工具链不熟,整个工作流程也很粗糙。

把过程记录下来,一方面是给自己做个备忘,另一方面也想和大伙交流一下,看看有没有什么更好用的工具,思路有没有问题,或者有什么更好的方法。


更新

干就干完,最后还是静下心来重新看了下逻辑,再结合 @skrets 老哥发出来的码,把整个流程走完了,感谢老哥的帮助。之前其实只差临门一脚,根本没注意到这边的base32 decode 是用了自定义的字符表的

    do
    {
      *((_BYTE *)&xmmword_10028E0D0 + (unsigned __int8)aAbcdefghjklmnp[v1]) = v1;
      ++v1;
    }
    while ( v1 != 32 );
    // aAbcdefghjklmnp DCB "ABCDEFGHJKLMNPQRSTUVWXYZ23456789",0 

有uv的话可以直接uv run --with pycryptodome main.py ,不用uv的话就先安装一下

目前只知道他有个黑名单会下载下来,但是没注意是不是会上传,所以还是建议断掉他的网,推荐LuLu

import hashlib

from Crypto.PublicKey import RSA
from Crypto.Util.number import bytes_to_long, long_to_bytes

PRIVATE_KEY = bytes.fromhex("30818a020100021a00b78ded78e15727da2d6a4224347c29effa13629cbc02881ea302030100010219153cffd02fcaa4dad2cc6bb0bf9963fe81096a58bcc6b8cd71020d0ca5e21ff874b1bab5fef40829020d0e833628e50732b7b7e987b9eb020d088c37350a11bb6786c5e62079020d05b77a2631d1dc10b2c9b34571020d0186f14ab61142b81329c8cd5d")
PUBLIC_KEY = bytes.fromhex("3035300D06092A864886F70D01010105000324003021021A00B78DED78E15727DA2D6A4224347C29EFFA13629CBC02881EA30203010001")
BASE32_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"
CHAR_MAP = {char: idx for idx, char in enumerate(BASE32_ALPHABET)}

def encode_custom_base32(data: bytes, pad: bool = True) -> str:
    bits = 0
    bit_buffer = 0
    encoded = []
    for byte in data:
        bit_buffer = (bit_buffer << 8) | byte
        bits += 8
        while bits >= 5:
            bits -= 5
            index = (bit_buffer >> bits) & 0b11111
            encoded.append(BASE32_ALPHABET[index])
    if bits > 0:
        index = (bit_buffer << (5 - bits)) & 0b11111
        encoded.append(BASE32_ALPHABET[index])
    if pad:
        while len(encoded) % 8 != 0:
            encoded.append('=')
    return ''.join(encoded)

def decode_custom_base32(encoded: str) -> bytes:
    encoded = encoded.strip("=").upper()
    for c in encoded:
        if c not in CHAR_MAP:
            raise ValueError(f"Invalid character in input: {c}")
    bits = 0
    bit_buffer = 0
    decoded_bytes = bytearray()
    for char in encoded:
        bit_buffer = (bit_buffer << 5) | CHAR_MAP[char]
        bits += 5
        while bits >= 8:
            bits -= 8
            decoded_bytes.append((bit_buffer >> bits) & 0xFF)
    return bytes(decoded_bytes)

def strip_pkcs1_padding(data: bytes) -> bytes:
    if data[0] != 0x01:
        raise ValueError("Not PKCS#1 v1.5 padding")
    sep = data.find(b'\x00', 1)
    if sep < 0:
        raise ValueError("Malformed PKCS#1 v1.5 block")
    return data[sep + 1:]

def pkcs1_padding(message: bytes, total_length: int) -> bytes:
    max_msg_len = total_length - 11
    if len(message) > max_msg_len:
        raise ValueError("Message too long")
    ps_length = total_length - len(message) - 3
    ps = b'\xFF' * ps_length
    return b'\x00\x01' + ps + b'\x00' + message

def decrypt(payload: bytes, key: RSA.RsaKey) -> bytes:
    c = bytes_to_long(payload)
    m = pow(c, key.e, key.n)
    padded = long_to_bytes(m)
    return strip_pkcs1_padding(padded)

def encrypt(payload: bytes, key: RSA.RsaKey) -> bytes:
    k = (key.n.bit_length() + 7) // 8
    padded = pkcs1_padding(payload, k)
    m = bytes_to_long(padded)
    c = pow(m, key.d, key.n)
    return long_to_bytes(c, k)

def build_license_payload(
        customer_name: str,
        issue_date: int = 0,
        num_copies: int = 0,
        license_type: int = 1
) -> bytes:
    normalized_name = " ".join(customer_name.strip().split()).lower()
    name_hash = hashlib.sha1(normalized_name.encode('utf-8')).digest()[:10]
    payload = name_hash
    payload += issue_date.to_bytes(2, 'little')
    payload += num_copies.to_bytes(1, 'little')
    payload += license_type.to_bytes(1, 'little')
    return payload

def format_license_key(license_key: str) -> str:
    return '-'.join(license_key[i:i + 8] for i in range(0, len(license_key), 8))

def verify_license_key(license_key: str, customer_name: str, key: RSA.RsaKey) -> bool:
    try:
        decoded = decode_custom_base32(license_key)
        decrypted_payload = decrypt(decoded, key)
        normalized_name = " ".join(customer_name.strip().split()).lower()
        name_hash = hashlib.sha1(normalized_name.encode('utf-8')).digest()[:10]
        return decrypted_payload[:10] == name_hash
    except Exception as e:
        print(f"Error verifying license key: {e}")
        return False

def main():
    name = "52pojie.cn"
    print(f"Generating license key for: {name}")
    private_key = RSA.import_key(PRIVATE_KEY)
    public_key = RSA.import_key(PUBLIC_KEY)
    payload = build_license_payload(name)
    license_key = encode_custom_base32(encrypt(payload, private_key))
    if not verify_license_key(license_key, name, public_key):
        print("License key verification failed!")
        return
    print("License key generated successfully!")
    print(f"License key: {format_license_key(license_key)}")

if __name__ == "__main__":
    main()



免费评分

参与人数 24威望 +2 吾爱币 +121 热心值 +24 收起 理由
zm183 + 1 + 1 大神厉害
hupanzi + 1 + 1 热心回复!
keanexh + 1 + 1 我很赞同!
wxg0564 + 1 + 1 用心讨论,共获提升!
haihangs + 1 + 1 用心讨论,共获提升!
filanlu + 1 + 1 我很赞同!
Hmily + 2 + 100 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
Qchi + 1 + 1 我很赞同!
17633930692 + 1 + 1 用心讨论,共获提升!
skrets + 1 + 1 用心讨论,共获提升!
guo1995 + 1 我很赞同!
ehu4ever + 1 + 1 谢谢@Thanks!
enjoyfly666 + 1 热心回复!
elan + 1 + 1 我很赞同!
杨辣子 + 1 + 1 谢谢@Thanks!
MFC + 1 + 1 谢谢@Thanks!
jk998 + 1 + 1 热心回复!
Carinx + 1 + 1 用心讨论,共获提升!
mscsky + 1 + 1 热心回复!
liurunzhe + 1 + 1 热心回复!
DQQQQQ + 1 + 1 谢谢@Thanks!
jinlide1101 + 1 + 1 用心讨论,共获提升!
sonh + 1 + 1 热心回复!
wi5101 + 1 + 1 用心讨论,共获提升!

查看全部评分

本帖被以下淘专辑推荐:

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

推荐
skrets 发表于 2025-7-8 18:38
第一次弄macOS的app啊,已经很厉害了
注册机的思路没问题,拿我之前分析出来的跑了几个给大家玩玩
[Plain Text] 纯文本查看 复制代码
Name: 52pojie.cn
License:
Personal          K6NMKQDG-JKAV8ZTD-LQ7QZYZL-8RFGN5SF-CFSZE3W4
Family            CWY2SSXB-GG9D2VJ2-VYSVTYTA-WT3SUES9-PPH65BNB
Corporate         V24GC2PV-YKAXMQBJ-NTWUAZTP-3G7255RP-YY7AGPWE
Media & Press     XACTGWZ3-SLREHNS5-JUAH868A-H5ETHJZZ-5TA7GRVA
Friend            T7N6JVKD-VJXQKJSX-XQEZCP8M-ZJXPWPSS-TJS4TNBF
Apple Employee    SEZBVP44-59NREMSL-NSA2LQL8-SZ6VXJ4J-Y5AED5NJ
推荐
 楼主| 0xBrian 发表于 2025-7-8 19:18 |楼主
skrets 发表于 2025-7-8 18:38
第一次弄macOS的app啊,已经很厉害了
注册机的思路没问题,拿我之前分析出来的跑了几个给大家玩 ...

硬啊哥们儿!我折腾了半天那个key死活过不去,耐心耗尽了就直接开始patch了。我还是再去看看争取把这个写出来吧
沙发
shsww 发表于 2025-7-8 06:09
3#
daoye9988 发表于 2025-7-8 07:12
进来学习大佬作品
4#
wi5101 发表于 2025-7-8 08:22
学习了,截图好清晰
5#
catter2000 发表于 2025-7-8 08:35
思路清晰,还这么谦虚
6#
laoa2025 发表于 2025-7-8 08:40
这个是苹果调试?
7#
whisky2011 发表于 2025-7-8 08:46
这个苹果的软件啊
8#
HA19683 发表于 2025-7-8 08:48
感谢分享,在黑苹果系统上尝试一下
9#
pulinstan 发表于 2025-7-8 08:53
思路清晰,值得学习
10#
caoyunlong1118 发表于 2025-7-8 08:56
能走通第一步,就说明离成功不远了。
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - 52pojie.cn ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2026-7-10 22:54

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表