本文所述内容仅面向合法授权场景下的软件逆向分析、安全研究与教学演示,不提供、不鼓励、不支持任何形式的非法破解、商业软件盗版、授权绕过、恶意攻击或侵犯他人权益的行为。读者应在遵守法律法规及软件许可协议的前提下进行学习和实验。作者不对任何未经授权或违法使用本文内容所导致的后果承担责任。
前言
买断的产品应该享有自主控制权,霞葉一直是这么认为的。这就是为什么我之前一直会选择能解锁 Bootloader、Root 的手机,选择能获取 Console 的路由器,选择能接入 HA 或开放协议的 IoT 设备,选择不锁 BIOS 且有 HDMI 接口的绿联 NAS。我是飞牛和绿联 NAS 双持用户,飞牛的开放性不必多说,适配了一众 x86 设备,活跃的论坛氛围,开放第三方应用社区(conversun/fnos-apps),甚至之前还对所有人开放过任意路径访问。飞牛的发展离不开社区的支持,飞牛自己也明白这一点,至少从 2025 年 11 月 6 日起,飞牛就已开放第三方应用的开发,印证飞牛说的那句“把飞牛 fnOS 打造成 “存储系统界的 Windows””。

当然,作为绿联 NAS 主力用户,我还是希望绿联也能开放第三方应用的开发。然后发现我们的绿联也是不负众望的把开放第三方应用,放入了已规划中…然后也是毫不意外地,在半年后的 2026 年 5 月 22 日才正式上线开发者平台。嘛 Better late than never,既然开放了,那我也是第一时间就去看了开发文档,却发现这个开发平台很粗糙糊弄,还一堆繁文缛节。开发平台只给了一个 cli 打包工具,少得可怜的 UGOS 系统交互能力,没有测试用虚拟环境。更难绷的是,我在我自己的设备上开发和测试,竟然需要申请签名才能安装自己的应用,而且申请签名还要填写他那鸡巴巨麻烦的模板。没错,不是在上架时要申请,而是你想安装自己开发的应用都要申请,要看他们脸色。虽然但是,我还是在当天随便填了一份,交了申请,毫不意外地到今天都没有收到回复,没想到真的这么严格审核啊。

这种事情,社区当然会有很大的意见…吗?然而我去看了社区,发现只有寥寥俩三条帖子反映这个事情。对此,社区的(疑似官方)宣传口也是非常的闹钟式回复“不限制被人投毒挖矿咋办”。对于绿联这种固步自封、僵化腐朽的思想,我想引用社区的一句评价,“玩死自己罢了,真当自己是 Apple 了”。作为一个对电脑控制欲很强的人(笑点解析),这种事情当然容忍不了,怎么可能容忍得了。对它使用 ida 吧,似乎有人这么对我说到。经过分析最终绕过了 ugdev.sig 签名校验,在自己的设备上安装了测试应用。不过,本文并不会给出绕过校验的成品,也不会给出任何直接协助绕过校验的工具,毕竟我也不想吃律师函。同时,也不希望被广泛的传播和滥用,如果后续引起绿联官方注意,可能会对程序进行混淆加固。


应用中心分析
从 Web 端的 upk 文件手动安装应用的入口来看,使用到签名文件 ugdev.sig 的是这个名为 应用中心 的程序。UGOS Pro 系统是基于 Debian 12 魔改的,ssh 登录之后,可以看到有很多 systemd service,根据名称猜测应用中心应该是 app_serv.service。程序是前后端分离的模式,后端是一个 stripped 无调试信息的 Go 二进制程序,好处是没有加壳、没有混淆,Go 的元数据都还在,可以恢复大量函数边界和函数名。那还说什么呢,ida 启动!虽是这么说,但这个时代真的还有人从头到尾手动分析吗,推荐使用 IDA Pro MCP,解放双手。
# 一些神秘符号和关键词app_serv/internal/ugdevcheck.Inspectapp_serv/services/devauth.(*serviceImpl).CheckAllowV1app_serv/services/devauth.(*serviceImpl).CheckFilegitlab.ugnas.com/ugos-pro/ugnas/sign.VerifyFileSignWithoutPubgitlab.ugnas.com/ugos-pro/ugnas/sign.VerifyFileSignV2gitlab.ugnas.com/ugos-pro/ugnas/sign.VerifyFileSignV3ugdev.sigdevAuthCheckAllowV1UGREEN-PKG-FORMATVerifyFileSignWithoutPubgetRootCaugreenRootImport.pubapp_serv/services/devauthapp_serv/internal/ugdevcheck首先,根据 .gopclntab 等元数据恢复函数名称,然后从函数名称中分析 ugdev.sig 校验机制即可。根据分析得知,系统支持三种版本的 UPK 安装包,分别是 V1, V2, V3,而此次开放的则是最老的 V1 格式 UPK,绿联在让人失望这一块还是不让人失望啊。V1 版本的校验走的是不同的链路,经过分析,UPK 安装的入口是在 services/upk.(*serviceImpl).upkPreCheck。 upkInfoWithV1Checker 是启用 V1 开发者授权检查的包信息入口;upkPreCheck 负责文件存在性、扩展名、系统版本和包格式策略;appmaker.GetUpkVersion 只读取包头 magic,判断 V1/V2/V3;loadUpk 则是在 precheck 通过之后才调用 appmaker.ParseUpk 和 appmaker.ExtractPackage,进入真正的包解析和展开阶段。版本号大于 108000000 的系统上,对于 V1 格式的包,会检查开发者授权,也就是绿联给你签发的 ugdev.sig。检查的函数是 services/devauth.(*serviceImpl).CheckAllowV1。整个流程如下:
用户上传 .upk -> services/upk.(*serviceImpl).upkInfoWithV1Checker -> services/upk.(*serviceImpl).upkPreCheck(..., allowV1Check=true) -> utils.Exists(path) -> 检查扩展名是否为 .upk -> device.GetSystemVersion() -> appmaker.GetUpkVersion(path) -> 如果 magic == "UGREEN-PKG-FORMAT" 且系统版本 >= 108000000 -> 调用 devAuth.CheckAllowV1() -> 检查 /ugreen/.config/ugdev.sig -> 授权失败则拒绝旧格式 -> 授权成功才继续进入 loadUpk / ParseUpk那么要做的事情就很明显了,顺着 CheckAllowV1 往下看,进入 services/devauth 这一层。值得关注的函数有这些:
services/devauth.initservices/devauth.getUserAuthFileservices/devauth.selectCurrentUserAuthFileservices/devauth.(*serviceImpl).CheckAllowV1services/devauth.(*serviceImpl).HasValidUserDevAuthFileservices/devauth.(*serviceImpl).CheckFileservices/devauth.inspectCheckAllowV1 确实是校验 ugdev.sig 的函数,但有趣的是,他是校验系统级授权的,对应文件 /ugreen/.config/ugdev.sig,直接影响 V1 包是否允许继续安装。而 HasValidUserDevAuthFile 才是校验用户级授权的,对应目录 <user home>/ugdev.sig,用于当前用户的开发者授权状态。哈哈,还有第二关。系统级和用户级校验流程分别如下:
services/devauth.(*serviceImpl).CheckAllowV1 -> utils.Exists("/ugreen/.config/ugdev.sig") -> 文件不存在: return (false, false) -> 文件存在: -> internal/ugdevcheck.Inspect("/ugreen/.config/ugdev.sig") -> 校验成功: logger.Infof("allow v1 upk by %s auth", source) return (true, true) -> 校验失败: logger.Errorf("auth v1 failed, source=%s, err=%v") return (false, true)services/devauth.(*serviceImpl).HasValidUserDevAuthFile(username) -> services/devauth.selectCurrentUserAuthFile(username) -> getUserAuthFile(username) -> os/user.Lookup(username) -> filepath.Join(user.HomeDir, "ugdev.sig") -> utils.Exists(<home>/ugdev.sig) -> services/devauth.inspect(path, source=current_user) -> os.Stat(path) -> internal/ugdevcheck.Inspect(path) -> localizeInspectError(err) -> personalUserDevAuthInspectShouldBlankBody(err) -> 返回当前用户是否有有效授权系统级和用户级校验最终都复用了 ugdevcheck.Inspect,真正的核心函数便是 internal/ugdevcheck.Inspect,这个函数会先调用 sign.VerifyFileSignWithoutPub 来验证文件签名,也就是说授权文件是 Payload + Signature 的形式,具体为:
ugdev.sig:+-----------------------------+------------------------------+| encrypted body prefix | outer signature/trailer tail || file[0 : len(file)-3072] | file[len(file)-3072 : end] |+-----------------------------+------------------------------+binary payload:+----------+----------------------+---------+| nonce | AES-GCM ciphertext | tag || 12 bytes | variable | 16 bytes|+----------+----------------------+---------+plaintext:{ "SN": "TESTSN123456", "Mac": "AA:BB:CC:DD:EE:FF", "DaysLimit": 365, "Operator": "operator name", "SignTime": 1735689600, "ExpireTime": 1767225600, "SignFile": "ugdev.sig"}很合理的结构,拿不到私钥的话,无论怎么也没法在不修改程序的情况下通过校验,总不能期待绿联自己不小心把私钥泄露出来吧。那么就只能 patch 二进制程序了,经过分析之后,要 patch 哪些点其实已经很清楚了,为了避免不必要的问题我就不直说了。总之核心的校验流程如下,顺带一提,由于有 normalizeMACAddress,所以 aa:bb:cc:dd:ee:ff、AA-BB-CC-DD-EE-FF、aabbccddeeff 这些写法都是可以的。
Inspect(path) -> utils.Exists(path) -> VerifyFileSignWithoutPub(path) -> os.ReadFile(path) -> 检查 len(file) - 3072 > 0 -> body = file[0 : len(file)-3072] -> openssl/aes256gcm.Decrypt(body) -> encoding/json.Unmarshal(plaintext, *SignInfo) -> device.GetSn() -> 比较 SignInfo.SN -> device.GetAllMac() -> normalizeMACAddress(SignInfo.Mac) -> normalizeMACAddress(each local MAC) -> time.Now() -> 检查过期时间 -> 返回 InspectResult实战测试
根据分析,对二进制程序patch之后,替换掉系统中原本的二进制程序,重启服务。可以看到设置里多出了”应用开发设置”,点击授权之后就可以安装自己的UPK包了。



在体验了测试应用能做到什么之后,我也许明白了绿联为什么连测试都要卡用户脖子,但我还是没法接收。正如最后一张图中的Banner文本,“开放生态,共赢未来”,飞牛就是一个很好的例子。绿联并不是苹果,也没有那么多开发者会专门来适配绿联,不会为了开发应用去填麻烦的申请表,更不会专门买一台真机测试。只有开放才能共赢,绿联的固步自封只会自取灭亡。最后,本文所述内容仅面向合法授权场景下的软件逆向分析、安全研究与教学演示,不提供、不鼓励、不支持任何形式的非法破解、商业软件盗版、授权绕过、恶意攻击或侵犯他人权益的行为。读者应在遵守法律法规及软件许可协议的前提下进行学习和实验。作者不对任何未经授权或违法使用本文内容所导致的后果承担责任。
看到便是缘分,希望读者不要传播,更不要到绿联官方那去跳脸。