李恒道 发表于 2024-6-30 18:24:34

onlyfans的OB解密及DRM过校验思路(七)

那接下来的问题就是我们该怎么读取onlyfans的密钥了

首先需要解密头

直接逐步定位打到了

```
            , Pe = e=>{
                try {
                  const t = {
                        ...(0,
                        F.A)(e)
                  };
                  t["app-token"] = te;
                  const s = V.A.getters["auth/authUserId"];
                  s && (t["user-id"] = s),
                  t["x-bc"] = fe(),
                  t["x-of-rev"] = "202406261341-9a802bb7ea";
                  const {hash: r} = V.A.state.hash;
                  return r && (t["x-hash"] = r),
                  t
                } catch (t) {
                  console.error(t)
                }
                return {}
            }
```

t初始化是sign和time

然后设置app-token,这里跟其他一致

user-id,设置用户id,等价于cookie的auth_id,也基本一致

t["x-bc"] = fe() 提取bcToken,老生常谈

t["x-of-rev"] = "202406261341-9a802bb7ea";固定值

const {hash: r} = V.A.state.hash; 从https://cdn2.onlyfans.com/hash/中读取

没难度啊我靠

```
x-hash: "mkVyQlWEXk/Vb0n/4iia1HdR+AeHJrzzR27MA+8="
x-of-rev:"202406261341-9a802bb7ea"
```

这两个参数要保持最新,如果旧版本的请求没补这两个或者hash过期

会导致请求正常但是DRM的Cookies是假的

好心机

但是直接往认证服务器发送包依然失败

这部分卡了我很久

只能去参考od-drm项目
https://github.com/sim0n00ps/OF-DRM/blob/089b7402dc1f6240993255b0492795bcd1114be8/OF DRM Video Downloader/Helpers/APIHelper.cs#L1199

找到了

```
                var resp1 = PostData(licenceURL, drmHeaders, new byte[] { 0x08, 0x04 });
                var certDataB64 = Convert.ToBase64String(resp1);
                var cdm = new CDMApi();
                var challenge = cdm.GetChallenge(pssh, certDataB64, false, false);
                var resp2 = PostData(licenceURL, drmHeaders, challenge);
                var licenseB64 = Convert.ToBase64String(resp2);
                cdm.ProvideLicense(licenseB64);
                List<ContentKey> keys = cdm.GetKeys();
```

可以看到先提交了一个0804,然后设置证书再处理解密部分

观察onlyfans的抓包也确实存在两次提交

因为我一直在搞解密部分大意了,没有闪!

关于设置服务器证书pywidevine也有函数,我们查看源码可以找到`set_service_certificate`函数

```
      Set a Service Privacy Certificate for Privacy Mode. (optional but recommended)

      The Service Certificate is used to encrypt Client IDs in Licenses. This is also
      known as Privacy Mode and may be required for some services or for some devices.
      Chrome CDM requires it as of the enforcement of VMP (Verified Media Path).

      We reject direct DrmCertificates as they do not have signature verification and
      cannot be verified. You must provide a SignedDrmCertificate or a SignedMessage
      containing a SignedDrmCertificate.

      Parameters:
            session_id: Session identifier.
            certificate: SignedDrmCertificate (or SignedMessage containing one) in Base64
                or Bytes form obtained from the Service. Some services have their own,
                but most use the common privacy cert, (common_privacy_cert). If None, it
                will remove the current certificate.

      Raises:
            InvalidSession: If the Session identifier is invalid.
            DecodeError: If the certificate could not be parsed as a SignedDrmCertificate
                nor a SignedMessage containing a SignedDrmCertificate.
            SignatureMismatch: If the Signature of the SignedDrmCertificate does not
                match the underlying DrmCertificate.

      Returns the Service Provider ID of the verified DrmCertificate if successful.
      If certificate is None, it will return the now-unset certificate's Provider ID,
      or None if no certificate was set yet.
```

那我们就继续研究一下原网页的代码,看看0801哪里来的

之前我们分析getLicense我们知道了接受消息的在

                  u.addEventListener("message", (e=>{
                        c.trigger({
                            type: "keymessage",
                            messageEvent: e
                        }),
                        "license-request" !== e.messageType && "license-renewal" !== e.messageType || a(o, e.message, d).then((e=>{
                            r(u.update(e).then((()=>{
                              c.trigger({
                                    type: "keysessionupdated",
                                    keySession: u
                              })
                            }
                            )).catch((e=>{
                              const t = {
                                    errorType: s.default.Error.EMEFailedToUpdateSessionWithReceivedLicenseKeys,
                                    keySystem: m
                              };
                              l(e, t)
                            }
                            )))
                        }
                        )).catch((e=>{
                            p(e)
                        }
                        ))
                  }

查找文档https://www.w3.org/TR/encrypted-media/

```
generateRequest
Generates a license request based on the initData. A message of type "license-request" or "individualization-request" will always be queued if the algorithm succeeds and the promise is resolved.

Parameter        Type        Nullable        Optional        Description
initDataType        DOMString        ✘        ✘        The Initialization Data Type of the initData.
initData        BufferSource        ✘        ✘        Initialization Data
```

根据文档的提示,生成我们可以定位到

```
                  u.generateRequest(n, i).catch((e=>{
                        const t = {
                            errorType: s.default.Error.EMEFailedToGenerateLicenseRequest,
                            keySystem: m
                        };
                        l(e, t),
                        p("Unable to create or initialize key session")
                  }
```

关于到底这两个是否有关联可以下断在u.generateRequest调用时将函数置为空函数()=>{},可以发现接收消息没有触发,证明了这两个函数没有关联

其中n是cenc,i是字节,我们一路往上堆栈回溯可以找到

```
            if (i) {
                e = {
                  attributes: n
                };
                const r = Te(t, "cenc:pssh");
                if (r) {
                  const t = Ie(r);
                  e.pssh = t && p(t)
                }
            }
```

其中p是将字符串转为字节数组

```
      function p(e) {
            for (var t = m(e), n = new Uint8Array(t.length), i = 0; i < t.length; i++)
                n = t.charCodeAt(i);
            return n
      }
```

根据调试首先传入的是wpd中较短的pssh,然后得到0804提交再获取证书

但是问题来了

0804到底怎么生成的?

我研究了几天还是没有得到答案

于是到处找人寻味

直到在forum.videohelp.com论坛得到了larley大神的解答!

https://forum.videohelp.com/threads/415095-How-to-simulate-the-generateRequest-function-through-python

```
The long PSSH is used for Microsoft's PlayReady and then short one is for Google's Widevine (that's what you're going to want to use).

The '08 04' (or CAQ= is base64) (or '\x08\x04' in python) is a fixed data value that can be sent to the same server (even same URL and 99% of the time even the same headers) from which you will receive your license.
```

0804竟然是generateRequest返回的固定值!

我在https://integration.widevine.com/diagnostics的生成widevine pssh试了几组

都返回了0804!

那么一切就通顺了

首先根据较短的pssh获得0804

然后将0804上传得到certData证书

再设置certData证书

然后上传pssh得到正确key

理论建立完毕

实践开始!



yhzc2023 发表于 2024-6-30 20:06:50

ggnb 继续继续

李恒道 发表于 2024-6-30 20:19:35

yhzc2023 发表于 2024-6-30 20:06
ggnb 继续继续

我都快心态炸了....
快半个月了还没结束
又卡在授权证书了
页: [1]
查看完整版本: onlyfans的OB解密及DRM过校验思路(七)