首页
社区
课程
招聘
破解emby-server
发表于: 2020-11-17 17:05 44754

破解emby-server

2020-11-17 17:05
44754

现在很多服务的正常运行都依赖某个中心服务器,如果服务器升级或出现故障或者公司跑路,付费用户将无法继续正常使用。emby也是如此,程序要访问mb3admin.com确认你是付费用户才能使用更多的功能,不管你的个人网络还是他们服务器出问题,都将不能使用付费功能。

 

网络上有一些解决方案,但大都需要搭建一个nginx伪装站点,生成ssl证书,emby服务器上添加证书并且修改hosts文件,客户端也需要修改hosts或者重定向mb3admin.com到伪站……我认为这样破解太过复杂了,用别人的伪站又担心不稳定。

 

所以,就有了今天的尝试,修改emby程序,让其在不设置伪站的情况下也能使用最基本的是付费功能。

破解过程

需要用到下面两个工具:

emby-server在Linux系统中安装于/opt/emby-server/,把此文件夹复制到hack-emby目录,并在此目录执行下面的脚本,美化javascript代码,以方便阅读。

1
2
3
4
5
6
7
8
#!/bin/bash
 
for f in $(find emby-server/ -name "*.js")
do
    echo "$f"
    js-beautify "$f" > tmp
    mv tmp "$f"
done

在浏览器打开emby,在Emby Premiere页面随便输入一个key按保存后提示Emby Premiere key is missing or invalid.

 

图片描述

 

搜索定义并调用这个字符串的位置,发现仅在system/dashboard-ui/embypremiere/embypremiere.html中有调用这个字符串,同目录有个embypremiere.js,根据MediaBrowser.Model.dll中的某些函数返回值,执行相关的判断。修改MediaBrowser.Model.dll中的get_IsMBSupporter和get_SupporterKey的返回值后不再提示这个错误,不过也不像付费用户一样显示。

 

图片描述

 

还需要修改emby-server/system/dashboard-ui/embypremiere/embypremiere.js中的load(page)函数,去除访问mb3admin.com相关的代码,并且硬编码返回给付费用户的json数据才能正常显示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function load(page) {
    var apiClient;
    loading.show(), (apiClient = ApiClient).getJSON(apiClient.getUrl("Plugins/SecurityInfo")).then(function(info) {
        var key, postData;
        page.querySelector("#txtSupporterKey").value = info.SupporterKey || "", page.querySelector("#txtSupporterKey").classList.remove("invalidEntry"), page.querySelector(".notSupporter").classList.add("hide")
                var statusInfo = {"deviceStatus":"0","planType":"Lifetime","subscriptions":{}};
                var statusLine, indicator = page.querySelector("#status-indicator .listItemIcon"),
                    extendedPlans = page.querySelector("#extended-plans");
                switch (extendedPlans.innerHTML = globalize.translate("MessagePremiereExtendedPlans", '<a is="emby-linkbutton" class="button-link" href="https://emby.media/premiere-ext.html" target="_blank">', "</a>"), statusInfo.deviceStatus) {
                    default:
                        statusLine = globalize.translate("MessagePremiereStatusGood", statusInfo.planType), indicator.classList.remove("expiredBackground"), indicator.classList.remove("nearExpiredBackground"), indicator.innerHTML = "&#xE5CA;", extendedPlans.classList.add("hide")
                }
                page.querySelector("#premiere-status").innerHTML = statusLine;
                var sub, subsElement = page.querySelector("#premiere-subs");
                statusInfo.subscriptions && 0 < statusInfo.subscriptions.length ? (page.querySelector("#premiere-subs-content").innerHTML = (subs = statusInfo.subscriptions, key = info.SupporterKey, subs.map(function(item) {
                    var itemHtml = "",
                        makeLink = item.autoRenew && "Stripe" === item.store,
                        tagName = makeLink ? "button" : "div";
                    return itemHtml += ("button" == tagName ? '<button type="button"' : "<div") + ' class="listItem listItem-button listItem-noborder' + (makeLink ? " lnkSubscription" : "") + '" data-feature="' + item.feature + '" data-key="' + key + '">', itemHtml += '<i class="listItemIcon md-icon">dvr</i>', itemHtml += '<div class="listItemBody two-line">', itemHtml += '<div class="listItemBodyText">', itemHtml += globalize.translate("ListItemPremiereSub", item.planType, item.expDate, item.store), itemHtml += "</div>", itemHtml += '<div class="listItemBodyText listItemBodyText-secondary">', itemHtml += globalize.translate("Stripe" === item.store ? item.autoRenew ? "LabelClickToCancel" : "LabelAlreadyCancelled" : "LabelCancelInfo", item.store), itemHtml += "</div>", itemHtml += "</div>", itemHtml += "</" + tagName + ">"
                })), (sub = page.querySelector(".lnkSubscription")) && sub.addEventListener("click", cancelSub), subsElement.classList.remove("hide")) : subsElement.classList.add("hide"), page.querySelector(".isSupporter").classList.remove("hide")
            //var subs, key
    }), loading.hide()
}

图片描述

 

这里并不是最重要的,也许恰恰是最不重要的,因为我修改相关的代码后,Dashboard页面并没有显示徽章,不能使用付费主题,不能看live tv,右上角的升级会员按钮没有消失。这个程序以前是开源的,后来闭源后越来越不好破解了!(不过javascript和C#跟开源没啥区别)


显示徽章

Dashboard中徽章位于emby-server/system/dashboard-ui/css/images/supporter/supporterbadge.png,脚本system/dashboard-ui/dashboard/dashboard.js中的renderSupporterIcon函数使用了这个图标,调用这个函数的代码段为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function() {
    var apiClient = window.ApiClient;
    return apiClient ? connectionManager.getRegistrationInfo("themes", apiClient, {
        viewOnly: !0
    }).then(function(result) {
        return {
            IsMBSupporter: !0
        }
    }, function() {
        return {
            IsMBSupporter: !1
        }
    }) : Promise.reject()
}().then(function(pluginSecurityInfo) {
    DashboardPage.renderSupporterIcon(page, pluginSecurityInfo);
    var html, supporterPromotionElem = page.querySelector(".supporterPromotion"),
        isSupporter = pluginSecurityInfo.IsMBSupporter;

只要把IsMBSupporter: !1改为IsMBSupporter: !0就可以显示徽章了。修改emby-server/system/dashboard-ui/bower_components/emby-apiclient/connectionmanager.js的getRegistrationInfo应该会更好,因为多处代码都调用了这个函数!

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
key: "getRegistrationInfo",
value: function(feature, apiClient, options) {
    var params =
        serverId: apiClient.serverId(),
        deviceId: this.deviceId(),
        deviceName: this.deviceName(),
        appName: this.appName(),
        appVersion: this.appVersion(),
        embyUserName: ""
    };
    (options = options || {}).viewOnly && (params.viewOnly = options.viewOnly);
    var cacheKey = getCacheKey(feature, apiClient, options),
        regInfo = JSON.parse(this.appStorage.getItem(cacheKey) || "{}"),
        timeSinceLastValidation = Date.now() - (regInfo.lastValidDate || 0); 
    if (timeSinceLastValidation <= 864e5) return console.log("getRegistrationInfo returning cached info"), Promise.resolve();
    var regCacheValid = timeSinceLastValidation <= 864e5 * (regInfo.cacheExpirationDays || 7); 
    params.embyUserName = apiClient.getCurrentUserName();
    var currentUserId = apiClient.getCurrentUserId();
    if (currentUserId && "81f53802ea0247ad80618f55d9b4ec3c" === currentUserId.toLowerCase() && "21585256623b4beeb26d5d3b09dec0ac" === params.serverId.toLowerCase()) return Promise.reject();
    var appStorage = this.appStorage,
        getRegPromise = ajax({
            url: "https://mb3admin.com/admin/service/registration/validateDevice?" + paramsToString(params),
            type: "POST",
            dataType: "json"
        }).then(function(response) {
            return appStorage.setItem(cacheKey, JSON.stringify({
                lastValidDate: Date.now(),
                deviceId: params.deviceId,
                cacheExpirationDays: response.cacheExpirationDays
            })), Promise.resolve()
        }, function(response) {
            var status = (response || {}).status;
            return console.log("getRegistrationInfo response: " + status), 403 === status ? Promise.reject("overlimit") : status && status < 500 ? Promise.reject() : function(err) {
                if (console.log("getRegistrationInfo failed: " + err), regCacheValid) return console.log("getRegistrationInfo returning cached info"), Promise.resolve();
                throw err  
            }(response)
        });
    return regCacheValid ? (console.log("getRegistrationInfo returning cached info"), Promise.resolve()) : getRegPromise
}

if (timeSinceLastValidation <= 864e5) return console.log("getRegistrationInfo returning cached info"), Promise.resolve();中的timeSinceLastValidation <= 864e5替换为true,确保始终返回本地缓存中的数据,而不再需要找服务器验证。

 

不过程序可能为了防止这个js文件被修改,在emby-server/system/dashboard-ui/app.js中有这么一段代码:

1
define("connectionManagerFactory", [], getDynamicImport("./bower_components/emby-apiclient/connectionmanager.js"))

运行时从emby-server/system/Emby.Web.dll中动态导入这个js文件,而不是使用emby-server/system/dashboard-ui/bower_components/emby-apiclient/connectionmanager.js,使用dnSpy的Hex Editor修改这几个字节就可以了。


破解会员功能

上面两个并没那么重要,上传、下载、电视直播和dvr这类功能才是更加实用的,这些都跟validateFeature有关系。

 

同样,这个函数不仅存在于system/dashboard-ui/modules/registrationservices/registrationservices.js,在Emby.Web.dll中也有备份,我们要修改后者才能生效。

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
{
       validateFeature: function(feature, options) {
           return options = options || {}, console.log("validateFeature: " + feature), iapManager.isUnlockedByDefault(feature, options).then(function() {
               return showPeriodicMessageIfNeeded(feature)
           }, function() {
               var unlockableFeatureCacheKey = "featurepurchased-" + feature;
               if ("1" === appSettings.get(unlockableFeatureCacheKey)) return showPeriodicMessageIfNeeded(feature);
               var unlockableProduct = iapManager.getProductInfo(feature);
               if (unlockableProduct) {
                   var unlockableCacheKey = "productpurchased-" + unlockableProduct.id;
                   if (unlockableProduct.owned) return appSettings.set(unlockableFeatureCacheKey, "1"), appSettings.set(unlockableCacheKey, "1"), showPeriodicMessageIfNeeded(feature);
                   if ("1" === appSettings.get(unlockableCacheKey)) return showPeriodicMessageIfNeeded(feature)
               }
               var unlockableProductInfo = unlockableProduct ? {
                   enableAppUnlock: !0,
                   id: unlockableProduct.id,
                   price: unlockableProduct.price,
                   feature: feature
               } : null;
               return iapManager.getSubscriptionOptions().then(function(subscriptionOptions) {
                   if (0 < subscriptionOptions.filter(function(p) {
                           return p.owned
                       }).length) return Promise.resolve();
                   var registrationOptions = {
                       viewOnly: options.viewOnly
                   };
                   return connectionManager.getRegistrationInfo(iapManager.getAdminFeatureName(feature), connectionManager.currentApiClient(), registrationOptions).catch(function(errorResult) {
                       return !1 === options.showDialog ? Promise.reject() : ("overlimit" === errorResult && (alertPromise = alertText("Your Emby Premiere device limit has been exceeded. Please check with the owner of your Emby Server and have them contact Emby support at support@emby.media if necessary.").catch(function() {
                           return Promise.resolve()
                       })), (alertPromise = alertPromise || Promise.resolve()).then(function() {
                           var dialogOptions = {
                               title: globalize.translate("HeaderUnlockFeature"),
                               feature: feature
                           };
                           return currentValidatingFeature = feature, showInAppPurchaseInfo(subscriptionOptions, unlockableProductInfo, dialogOptions)
                       }));
                       var alertPromise
                   })
               })
           })
       },
       showPremiereInfo: showPremiereInfo
   }

修改这个函数的返回值,并保存。替换掉系统中相关的文件,刷新浏览器缓存,就可以使用会员的大部分功能了。

 

图片描述

 

上图是破解前,即使正版程序,在局域网无法访问mb3admin.com时,可能也无法使用付费功能。下图为破解后,emby-server不再访问mb3admin.com而是直接返回缓存中/硬编码在dll和js中的数据。

 

图片描述

 

 

图片描述

 

开发不易,不再公开提供破解版本。仅供学习交流

参考链接


[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)

最后于 2020-11-30 22:00 被Explorerl编辑 ,原因: 完善破解过程
收藏
免费 0
支持
分享
最新回复 (4)
雪    币:
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
2
不行了,最后那个步骤没说是改成什么,而且改了也不行,实测还有一个dll文件需要修改才能硬解和转换下载。
2021-4-11 19:49
0
雪    币: 433
活跃值: (438)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
3
新版好像是做了eazfuscator的混淆
2021-9-15 23:04
0
雪    币: 0
活跃值: (26)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
4
根据大佬的思路,成功破解
2021-10-11 17:22
0
雪    币: 86
活跃值: (237)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
5
记得咸鱼上有人挂VPN协议破解
2021-10-12 11:24
0
游客
登录 | 注册 方可回帖
返回
//