上一主题 下一主题
ScriptCat,新一代的脚本管理器脚本站,与全世界分享你的用户脚本油猴脚本开发指南教程目录
返回列表 发新帖

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

[复制链接]
  • TA的每日心情
    擦汗
    2024-7-16 09:20
  • 签到天数: 192 天

    [LV.7]常住居民III

    691

    主题

    5563

    回帖

    6462

    积分

    管理员

    非物质文化遗产社会摇传承人

    积分
    6462

    荣誉开发者管理员油中2周年生态建设者喜迎中秋

    发表于 2024-6-21 05:18:09 | 显示全部楼层 | 阅读模式

    图片.png
    首先第一层是标准的OB加密
    我们先大概规整一下代码

        traverse(ast, {
            CallExpression(path) {
                if (path.node.arguments.length === 2) {
                    const type0 = path.node.arguments[0].type
                    const type1 = path.node.arguments[1].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[0].type !== 'ReturnStatement') {
                            return
                        }
                        const funcArgs0 = funcNode.params[0].name
                        const funcArgs1 = funcNode.params[1].name
                        const bodyCallArgs = funcNode.body.body[0].argument.arguments
                        let isSwap = false
                        for (let index = 0; index < bodyCallArgs.length; index++) {
                            const item = bodyCallArgs[index];
                            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[0]
                        const secondIdentifier = path.node.arguments[1]
                        let newCalleeArgs = [handleExpression(bodyCallArgs[0], isSwap ? secondIdentifier : firstIdentifier), handleExpression(bodyCallArgs[1], isSwap ? firstIdentifier : secondIdentifier)]
                        let newNode = types.callExpression(funcNode.body.body[0].argument.callee, newCalleeArgs);
                        path.replaceInline(newNode)
                    }
                }
            },
        });

    然后获取解密的函数,这里因为比较偷懒,所以直接使用了正则表达式计算关键函数

    function generatorHandleCrackStringFunc(text) {
        const matchResult = text.match(/\d{4,}\);\s?(function.*),\s?[A-Za-z].[A-Za-z]\s?=\s?[A-Za-z]/)
        if (matchResult.length !== 2) {
            throw new Error('代码解析失败!')
        }
        const funcName = matchResult[1].match(/function ([A-Za-z])\([A-Za-z],\s?[A-Za-z]\).*(?=abc)/)[1]
        return {
            crackName: funcName,
            crackCharFunc: new Function([], matchResult[1] + ';return function(num,char){return ' + funcName + '(num, char)}')()
        }
    }

    然后调用解密函数

        traverse(ast, {
            CallExpression(path) {
                if (path.node.arguments.length === 2) {
                    if (path.node.callee.name !== name) {
                        return
                    }
                    if (path.node.arguments[0].type !== 'NumericLiteral') {
                        return;
                    }
                    if (path.node.arguments[1].type !== 'StringLiteral') {
                        return;
                    }
                    const nodeResult = handleStringFunc(path.node.arguments[0].value, path.node.arguments[1].value)
                    path.replaceInline(types.stringLiteral(nodeResult))
                }
            },
        });

    然后对解密后的字符串和数字等做一下合并

        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[0].argument
                                //函数
                                if (originNode.type === 'CallExpression') {
                                    replaceAst = types.callExpression(parentNode.arguments[0], [...parentNode.arguments].splice(1))
                                } else if (originNode.type === 'BinaryExpression') {
                                    replaceAst = types.binaryExpression(originNode.operator, parentNode.arguments[0], parentNode.arguments[1])
                                }
                            } 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);

    我们可以从已经解密的文件里提取一些关键字符串

        const mathRsult = code.match(/\[\"(.*)\", [a-zA-Z]\[\"time\"\][\s\S]*\[\"sign\"\] = \[\"([0-9]*)\".*function \(([a-zA-Z])\) {([\s\S]*)}\([a-zA-Z]\)\,.*?"([a-zA-Z0-9]{3,})"/)
        if (mathRsult.length !== 6) {
            throw new Error('密钥解析失败!')
        }
        const signPrefix = mathRsult[2]
        const signEnd = mathRsult[5]
        const prefixToken = mathRsult[1]
        const hashFunc = new Function(mathRsult[3], mathRsult[4])

    接下来直接调试可以解出来BCToken的算法

        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加密算法也可以解出来了

        function generateSha({ url, auth_id }) {
            const fixPrefix = prefixToken;
            let time = +new Date();
            const toeknURL = [fixPrefix, time, url, auth_id || 0].join(`\n`);
            const hash = sha1.create();
            hash.update(toeknURL);
            return {
                token: hash.hex(),
                time: time
            }
        }
           function  getSign({ url, auth_id }) {
                const { time, token } = generateSha({ url, auth_id })
                return {
                    sign: [signPrefix, token, hashFunc(token), signEnd].join(':'),
                    time: time
                }
            }

    那基本的算法解密就搞定了,但是最近还更新了DRM
    图片.png
    其中给了一个mpt和m3u8
    分别有不同的密钥
    根据测试DRM的密钥是需要写在Cookies里的
    但是诡异的事情来了
    postman可以测试成功,cmd测试失败,代码测试失败,powershell测试成功
    ffmpeg测试也失败

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

    于是在https://github.com/nodejs/undici/issues/1983
    抄了一段,改成onlyfans的,这里就按下不表了

    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[1],
        defaultCiphers[2],
        defaultCiphers[0],
        ...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
    那果断切一下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[name]}`);
      }
    
    });
    
    req.on("data", chunk => {
      data += chunk;
    });
    req.on("end", () => {
      console.log(data);
      client.close();
    });
    req.end();

    果然成功读取到数据!
    图片.png
    但是问题又来了,即使现在我们找到了原因
    但是ffmpeg不支持HTTP2的m3u8读取
    那我们可能需要起一个服务器做1.1到2的代理
    但是这样太麻烦了,有没有更简单的办法?

    待续.....

    混的人。
    ------------------------------------------
    進撃!永遠の帝国の破壊虎---李恒道

    入驻了爱发电https://afdian.net/a/lihengdao666
    个人宣言:この世界で私に胜てる人とコードはまだ生まれていません。死ぬのが怖くなければ来てください。
  • TA的每日心情
    擦汗
    2024-3-25 15:30
  • 签到天数: 135 天

    [LV.7]常住居民III

    4

    主题

    109

    回帖

    206

    积分

    荣誉开发者

    积分
    206

    荣誉开发者油中2周年

    发表于 2024-6-21 08:28:08 | 显示全部楼层
    他真的我哭死
    道哥凌晨5点就来分享技术文章了
    可恃唯我
    回复

    使用道具 举报

  • TA的每日心情
    开心
    2024-3-13 10:14
  • 签到天数: 211 天

    [LV.7]常住居民III

    298

    主题

    4094

    回帖

    3969

    积分

    管理员

    积分
    3969

    管理员荣誉开发者油中2周年生态建设者喜迎中秋油中3周年挑战者 lv2

    发表于 2024-6-21 11:00:28 | 显示全部楼层
    哥哥最近是在干啥呢?
    上不慕古,下不肖俗。为疏为懒,不敢为狂。为拙为愚,不敢为恶。/ 微信公众号:一之哥哥
    回复

    使用道具 举报

  • TA的每日心情
    擦汗
    2024-7-16 09:20
  • 签到天数: 192 天

    [LV.7]常住居民III

    691

    主题

    5563

    回帖

    6462

    积分

    管理员

    非物质文化遗产社会摇传承人

    积分
    6462

    荣誉开发者管理员油中2周年生态建设者喜迎中秋

    发表于 2024-6-21 14:31:28 | 显示全部楼层
    cocang 发表于 2024-6-21 08:28
    他真的我哭死
    道哥凌晨5点就来分享技术文章了

    hhhh,搞精神了
    混的人。
    ------------------------------------------
    進撃!永遠の帝国の破壊虎---李恒道

    入驻了爱发电https://afdian.net/a/lihengdao666
    个人宣言:この世界で私に胜てる人とコードはまだ生まれていません。死ぬのが怖くなければ来てください。
    回复

    使用道具 举报

  • TA的每日心情
    擦汗
    2024-7-16 09:20
  • 签到天数: 192 天

    [LV.7]常住居民III

    691

    主题

    5563

    回帖

    6462

    积分

    管理员

    非物质文化遗产社会摇传承人

    积分
    6462

    荣誉开发者管理员油中2周年生态建设者喜迎中秋

    发表于 2024-6-21 14:31:55 | 显示全部楼层
    王一之 发表于 2024-6-21 11:00
    哥哥最近是在干啥呢?

    尝试硬过google wvd l3 drm
    也是世界目前最流行的drm版权系统
    混的人。
    ------------------------------------------
    進撃!永遠の帝国の破壊虎---李恒道

    入驻了爱发电https://afdian.net/a/lihengdao666
    个人宣言:この世界で私に胜てる人とコードはまだ生まれていません。死ぬのが怖くなければ来てください。
    回复

    使用道具 举报

  • TA的每日心情

    2024-8-13 20:59
  • 签到天数: 307 天

    [LV.8]以坛为家I

    12

    主题

    63

    回帖

    638

    积分

    荣誉开发者

    积分
    638

    荣誉开发者生态建设者

    发表于 2024-6-21 23:34:29 | 显示全部楼层
    不明觉厉。我想道哥破drm干嘛,原来是要搞onlyfans
    当冥想的日子飞逝,喧嚣的日子把我们唤去,且在此地留下些微的痕迹
    回复

    使用道具 举报

  • TA的每日心情
    擦汗
    2024-7-16 09:20
  • 签到天数: 192 天

    [LV.7]常住居民III

    691

    主题

    5563

    回帖

    6462

    积分

    管理员

    非物质文化遗产社会摇传承人

    积分
    6462

    荣誉开发者管理员油中2周年生态建设者喜迎中秋

    发表于 2024-6-22 01:19:35 | 显示全部楼层
    朱焱伟 发表于 2024-6-21 23:34
    不明觉厉。我想道哥破drm干嘛,原来是要搞onlyfans

    绕来绕去块一个星期了
    最近几天刚有一点突破
    本地cdm解密又卡住了
    不知道还能不能推进
    混的人。
    ------------------------------------------
    進撃!永遠の帝国の破壊虎---李恒道

    入驻了爱发电https://afdian.net/a/lihengdao666
    个人宣言:この世界で私に胜てる人とコードはまだ生まれていません。死ぬのが怖くなければ来てください。
    回复

    使用道具 举报

    发表回复

    本版积分规则

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