李恒道 发表于 2024-6-21 05:18:09

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

![图片.png](data/attachment/forum/202406/21/050511a3zzedr5rd0535dd.png)
首先第一层是标准的OB加密
我们先大概规整一下代码
```js
    traverse(ast, {
      CallExpression(path) {
            if (path.node.arguments.length === 2) {
                const type0 = path.node.arguments.type
                const type1 = path.node.arguments.type
                const isLikelyNumber = (type) => {
                  return type === 'UnaryExpression' || type === 'NumericLiteral'
                }
                if ((type0 === 'StringLiteral' && isLikelyNumber(type1)) || (type1 === 'StringLiteral' && isLikelyNumber(type0))) {
                  const funcBinding = path.scope.getBinding(path.node.callee.name)
                  const funcNode = funcBinding.path.node
                  if (funcNode?.params?.length !== 2) {
                        return
                  }
                  if (funcNode.body.body.length !== 1) {
                        return
                  }
                  if (funcNode.body.body.type !== 'ReturnStatement') {
                        return
                  }
                  const funcArgs0 = funcNode.params.name
                  const funcArgs1 = funcNode.params.name
                  const bodyCallArgs = funcNode.body.body.argument.arguments
                  let isSwap = false
                  for (let index = 0; index < bodyCallArgs.length; index++) {
                        const item = bodyCallArgs;
                        if (item.type === 'Identifier') {

                            if (item.name === funcArgs0 && index === 1) {
                              isSwap = true
                            } else if (item.name === funcArgs1 && index === 0) {
                              isSwap = true
                            }
                            break;
                        }
                  }
                  const handleExpression = (bodyExpress, argsIdentifier) => {
                        if (bodyExpress.type !== 'BinaryExpression') {
                            return argsIdentifier
                        }
                        const handleIdentifier = (item) => {
                            if (item.type !== 'Identifier') {
                              return item
                            } else {
                              return argsIdentifier
                            }
                        }
                        const numAst = types.binaryExpression(bodyExpress.operator, handleIdentifier(bodyExpress.left), handleIdentifier(bodyExpress.right))
                        const numResult = eval(generator(numAst).code)
                        return types.numericLiteral(numResult)
                  }
                  const firstIdentifier = path.node.arguments
                  const secondIdentifier = path.node.arguments
                  let newCalleeArgs = , isSwap ? secondIdentifier : firstIdentifier), handleExpression(bodyCallArgs, isSwap ? firstIdentifier : secondIdentifier)]
                  let newNode = types.callExpression(funcNode.body.body.argument.callee, newCalleeArgs);
                  path.replaceInline(newNode)
                }
            }
      },
    });
```
然后获取解密的函数,这里因为比较偷懒,所以直接使用了正则表达式计算关键函数
```js
function generatorHandleCrackStringFunc(text) {
    const matchResult = text.match(/\d{4,}\);\s?(function.*),\s?.\s?=\s?/)
    if (matchResult.length !== 2) {
      throw new Error('代码解析失败!')
    }
    const funcName = matchResult.match(/function ()\(,\s?\).*(?=abc)/)
    return {
      crackName: funcName,
      crackCharFunc: new Function([], matchResult + ';return function(num,char){return ' + funcName + '(num, char)}')()
    }
}
```
然后调用解密函数
```js
    traverse(ast, {
      CallExpression(path) {
            if (path.node.arguments.length === 2) {
                if (path.node.callee.name !== name) {
                  return
                }
                if (path.node.arguments.type !== 'NumericLiteral') {
                  return;
                }
                if (path.node.arguments.type !== 'StringLiteral') {
                  return;
                }
                const nodeResult = handleStringFunc(path.node.arguments.value, path.node.arguments.value)
                path.replaceInline(types.stringLiteral(nodeResult))
            }
      },
    });
```
然后对解密后的字符串和数字等做一下合并
```js
    const handleObfs = {
      CallExpression: {
            exit(outerPath) {
                const node = outerPath.node.callee
                const parentPath = outerPath
                if (node?.object?.type === 'Identifier' && node?.property?.type === 'StringLiteral') {
                  const objBinding = outerPath.scope.getBinding(node.object.name)
                  if (objBinding === undefined) {
                        return;
                  }
                  const objNode = objBinding.path.node
                  const funcList = objNode.init?.properties ?? []
                  const funcInstance = funcList.find((item) => {
                        const keyName = item.key.name
                        return keyName === node.property.value
                  })
                  if (funcInstance) {
                        const parentNode = parentPath.node

                        let replaceAst = null
                        if (funcInstance.value.type === 'FunctionExpression') {
                            const originNode = funcInstance.value.body.body.argument
                            //函数
                            if (originNode.type === 'CallExpression') {
                              replaceAst = types.callExpression(parentNode.arguments, [...parentNode.arguments].splice(1))
                            } else if (originNode.type === 'BinaryExpression') {
                              replaceAst = types.binaryExpression(originNode.operator, parentNode.arguments, parentNode.arguments)
                            }
                        } else {
                            //字符串
                            debugger
                            replaceAst = types.stringLiteral(funcInstance.value.value)
                        }
                        if (replaceAst) {
                            parentPath.replaceWith(replaceAst)


                        }

                  }
                }
            }
      },
      MemberExpression: {
            enter(path) {
                const node = path.node
                if (node?.object?.type === 'Identifier' && node?.property?.type === 'StringLiteral') {
                  const objBinding = path.scope.getBinding(node.object.name)
                  if (objBinding === undefined) {
                        return;
                  }
                  const objNode = objBinding.path.node
                  const funcList = objNode.init?.properties ?? []
                  const funcInstance = funcList.find((item) => {
                        const keyName = item.key.name
                        return keyName === node.property.value
                  })
                  if (funcInstance) {
                        let replaceAst = null
                        if (funcInstance.value.type === 'StringLiteral') {
                            replaceAst = types.stringLiteral(funcInstance.value.value)
                        }
                        if (replaceAst) {
                            path.replaceWith(replaceAst)
                        }

                  }
                }
            }
      }
    }

    traverse(ast, handleObfs);
```
我们可以从已经解密的文件里提取一些关键字符串
```js
    const mathRsult = code.match(/\[\"(.*)\", \[\"time\"\][\s\S]*\[\"sign\"\] = \[\"(*)\".*function \(()\) {([\s\S]*)}\(\)\,.*?"({3,})"/)
    if (mathRsult.length !== 6) {
      throw new Error('密钥解析失败!')
    }
    const signPrefix = mathRsult
    const signEnd = mathRsult
    const prefixToken = mathRsult
    const hashFunc = new Function(mathRsult, mathRsult)
```
接下来直接调试可以解出来BCToken的算法
```js
    function generateBcToken() {
      if (bcToken !== "") {
            return bcToken
      }
      const V = () => 1e12 * Math.random()
      const UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36'
      const hash = sha1.create();
      const text = [(new Date).getTime(), V(), V(), UA].map(btoa).join(".")
      console.log(text)
      hash.update(text);
      bcToken = hash.hex()
      return bcToken
    }
```
Sign加密算法也可以解出来了
```js
    function generateSha({ url, auth_id }) {
      const fixPrefix = prefixToken;
      let time = +new Date();
      const toeknURL = .join(`\n`);
      const hash = sha1.create();
      hash.update(toeknURL);
      return {
            token: hash.hex(),
            time: time
      }
    }
       functiongetSign({ url, auth_id }) {
            const { time, token } = generateSha({ url, auth_id })
            return {
                sign: .join(':'),
                time: time
            }
      }
```
那基本的算法解密就搞定了,但是最近还更新了DRM
![图片.png](data/attachment/forum/202406/21/051045qdq8874jw78rs44m.png)
其中给了一个mpt和m3u8
分别有不同的密钥
根据测试DRM的密钥是需要写在Cookies里的
但是诡异的事情来了
postman可以测试成功,cmd测试失败,代码测试失败,powershell测试成功
ffmpeg测试也失败

我的第一反应可能是TLS指纹校验了
(这部分事后发现1.1也可以了,只要同ip就行,我也不确定到底是我测试错误还是后期改了)

于是在https://github.com/nodejs/undici/issues/1983
抄了一段,改成onlyfans的,这里就按下不表了
```js
const undici = require("undici")
const tls = require("tls")

// From https://httptoolkit.com/blog/tls-fingerprinting-node-js/
const defaultCiphers = tls.DEFAULT_CIPHERS.split(':');
const shuffledCiphers = [
    defaultCiphers,
    defaultCiphers,
    defaultCiphers,
    ...defaultCiphers.slice(3)
].join(':');

const connector = undici.buildConnector({ ciphers: shuffledCiphers })
const client = new undici.Client("https://en.zalando.de", { connect: connector })

undici.request("https://en.zalando.de/api/navigation", {
    dispatcher: client,
    headers: {
      "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36"
    }
}).then(async (res) => {
    const body = await res.body.json()
    console.log(body)
})

```
依然没有成功,这个时候还跟无头苍蝇一样打转,我认为可能是TLS因为Node修改的不彻底导致的,决定切换Go技术栈试试
于是找到了https://juejin.cn/post/7073264626506399751#heading-4
测试惊觉发现竟然是HTTTP2
于是返回抓包看了一眼
发现确实都是HTTP2!
![图片.png](data/attachment/forum/202406/21/051534xkyjkkmhyf5ykraj.png)
那果断切一下HTTP2的通信协议试一下
```
const http2 = require("http2");
const client = http2.connect("https://cdn3.onlyfans.com");

const req = client.request({
":method": "GET",
":path": "/dash/files/3/3f/XXX/XXX.mpd",
"accept": "*/*",
"accept-language": "zh-CN,zh;q=0.9",
"cache-control": "no-cache",
"pragma": "no-cache",
"priority": "u=1, i",
"sec-ch-ua": "\"Not/A)Brand\";v=\"8\", \"Chromium\";v=\"126\", \"Google Chrome\";v=\"126\"",
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": "\"Windows\"",
"sec-fetch-dest": "empty",
"sec-fetch-mode": "cors",
"sec-fetch-site": "same-site",
"cookie": "保护隐私",
"Referer": "https://onlyfans.com/",
"Referrer-Policy": "strict-origin-when-cross-origin"
});

let data = "";

req.on("response", (headers, flags) => {
for (const name in headers) {
    console.log(`${name}: ${headers}`);
}

});

req.on("data", chunk => {
data += chunk;
});
req.on("end", () => {
console.log(data);
client.close();
});
req.end();
```
果然成功读取到数据!
![图片.png](data/attachment/forum/202406/21/051705ao608brkdkasozhl.png)
但是问题又来了,即使现在我们找到了原因
但是ffmpeg不支持HTTP2的m3u8读取
那我们可能需要起一个服务器做1.1到2的代理
但是这样太麻烦了,有没有更简单的办法?
# 待续.....

cocang 发表于 2024-6-21 08:28:08

他真的我哭死
道哥凌晨5点就来分享技术文章了 {:4_94:}

王一之 发表于 2024-6-21 11:00:28

哥哥最近是在干啥呢?

李恒道 发表于 2024-6-21 14:31:28

cocang 发表于 2024-6-21 08:28
他真的我哭死
道哥凌晨5点就来分享技术文章了

hhhh,搞精神了

李恒道 发表于 2024-6-21 14:31:55

王一之 发表于 2024-6-21 11:00
哥哥最近是在干啥呢?

尝试硬过google wvd l3 drm
也是世界目前最流行的drm版权系统

朱焱伟 发表于 2024-6-21 23:34:29

不明觉厉。我想道哥破drm干嘛,原来是要搞onlyfans

李恒道 发表于 2024-6-22 01:19:35

朱焱伟 发表于 2024-6-21 23:34
不明觉厉。我想道哥破drm干嘛,原来是要搞onlyfans

绕来绕去块一个星期了
最近几天刚有一点突破
本地cdm解密又卡住了{:4_98:}
不知道还能不能推进
页: [1]
查看完整版本: onlyfans的OB解密及DRM过校验思路