吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 19895|回复: 230
上一主题 下一主题
收起左侧

[Web逆向] 某小说站点逆向还原文本——CSS反爬,AST解混淆-完美破解于20230409

    [复制链接]
跳转到指定楼层
楼主
hans7 发表于 2023-4-11 02:04 回帖奖励
本帖最后由 hans7 于 2023-4-11 02:07 编辑

引言

前几天视频刷到一个小说站点,其内容没啥营养,却使用了CSS反爬和OB混淆。于是我去读了一下它的算法。读懂并写出脚本后,感觉这个网站逆向难度较小,可以尝试完美破解然后水篇入门文,即:只需要给出小说某一章的html链接,就能获取其内容。

样本地址:aHR0cHM6Ly9nLmhvbmdzaHUuY29tL2NvbnRlbnQvMTIxMTAyLzI3NzM2MDkuaHRtbA==

技术栈:

  1. ts-nodetypescript
  2. babel处理AST。
  3. superagent发起网络请求。
  4. cheerio解析HTML文本。

作者:hans774882968以及hans774882968以及hans774882968

本文52pojie:https://www.52pojie.cn/thread-1772187-1-1.html

算法分析

小说内容的HTML结构很简单,每段都是一个p标签,文本中间有一些不和谐的span标签:

<div class="rdtext"fsize="16">
    <p><span class="context_kw6"></span>叫顾凡<span class="context_kw0"></span>顾<span class="context_kw7"></span>得<span class="context_kw4"></span>顾<span class="context_kw0"></span>平凡<span class="context_kw4"></span>凡(省略一些文字)</p>
</div>

小说的部分文字有缺失,需要用CSS::before伪元素渲染,比如:

.context_kw0::before {
    content: ",";
}

这样既不影响用户阅读,又增大了逆向难度。

面对这个样本,我能想到的切入点不多:全局搜索context_kwdocument,因为可以猜测JS代码需要动态创建style标签和添加CSS规则。发现context_kw在HTML文档的JS代码中只出现了1次,在一个巨大的常量串数组中。如果运气好,那个常量串没被混淆,可以看到context_kw出现在业务代码中。熟悉OB混淆套路的佬们都能立刻意识到,这段代码在做些见不得人的关键操作。

首先我们用AST还原一下(用到的cff, switchCFF等函数都来自我的开源项目):

import * as parser from '@babel/parser';
import { renameVars } from './rename_vars';
import generator from '@babel/generator';
import { getFile, writeOutputToFile } from './file_utils';
import { memberExpComputedToFalse } from './member_exp_computed_to_false';
import { translateLiteral } from './translate_literal';
import traverse from '@babel/traverse';
import {
  Node,
  isIdentifier,
  isNumericLiteral,
  stringLiteral, isStringLiteral
} from '@babel/types';
import { cff } from './remove_cff';
import { switchCFF } from './remove_switch_cff';

const jsCode = getFile('src/inputs/小说.ts');
const ast = parser.parse(jsCode);

// 如果常量表不止1处,则此代码不正确
function restoreStringLiteral (ast: Node, stringLiteralFuncs: string[], getStringArr: (idx: number) => string) {
  // 收集与常量串隐藏有关的变量
  traverse(ast, {
    VariableDeclarator (path) {
      const vaNode = path.node;
      if (!isIdentifier(vaNode.init) || !isIdentifier(vaNode.id)) return;
      if (stringLiteralFuncs.includes(vaNode.init.name)) {
        stringLiteralFuncs.push(vaNode.id.name);
      }
    }
  });
  traverse(ast, {
    CallExpression (path) {
      const cNode = path.node;
      if (!isIdentifier(cNode.callee)) return;
      const varName = cNode.callee.name;
      if (!stringLiteralFuncs.includes(varName)) return;
      const literalNode = cNode.arguments[0];
      if (cNode.arguments.length !== 1 || (!isNumericLiteral(literalNode) && !isStringLiteral(literalNode))) return;
      const idx = Number(literalNode.value);
      path.replaceWith(stringLiteral(getStringArr(idx)));
    }
  });
}
// 这里需要人工运行JS代码,获取最终的大数组
restoreStringLiteral(ast, ['_0x0a9e'], (idx: number) => {
  return [
    'pad', 'clamp', 'sigBytes', 'words', 'BXNBf', 'OMxlD', 'GhFlG', 'JxsFw', 'iksgN', 'qDbwG', 'prototype', 'spzgJ', 'test',
    'lo1c0tQyRk7E/Lr2p3puiAKrzgb8Absq4EWawXjoVfP230ItoMvvmsg3H8ccHG1u1qA+T/T4f3Rwi5j40osnuhQGtUj0w5rjN5FglNam4JRHNS126MHWX6+Zk/Aez8M7WttDCxtn6N6/pwWRtVat6vPkvmw9ETifmJ5C94R9hoGnDvNjntiKW6m5HPr+b/j0IvHCUJz8pX4ofi12NyD5aA==',
    'enc', 'Latin1', 'parse', 'B79CD410AF398F7A', 'window', 'location', 'href', '146385F634C9CB00', 'ZeroPadding', 'toString', 'Utf8', 'split', 'length', 'createElement', 'style', 'type', 'text/css', 'setAttribute', 'link', 'getElementsByTagName', 'gHLRp', 'CbiRt', 'oKMpY', 'parentNode', 'head', 'appendChild', '4|1|2|5|3|0', 'fromCharCode', 'NQvuJ', 'TYKEL', 'undefined', 'tfwZU', 'ffVsL', 'styleSheets', 'addRule', '.context_kw', '::before', 'content: "', 'insertRule', '::before{content: "'
  ][idx];
});

cff(ast);
switchCFF(ast);

memberExpComputedToFalse(ast);
renameVars(
  ast,
  (name:string) => name.substring(0, 3) === '_0x',
  {}
);
translateLiteral(ast);

const { code } = generator(ast);
writeOutputToFile('小说_out.js', code);

还原后算法分析过程就畅通无阻了,甚至不需要动态调试。

给样式表添加用于展示缺失文本的CSS规则:

for (var i = 0; i < words.length; i++) {
  try {
    document.styleSheets[0].addRule(".context_kw" + i + "::before", "content: \"" + words[i] + "\"");
  } catch (v72) {
    document.styleSheets[0].insertRule(".context_kw" + i + "::before{content: \"" + words[i] + "\"}", document.styleSheets[0].cssRules.length);
  }
}

AES解密数据如下。data是加密数据,每个html链接都不一样,猜测是先由模板引擎渲染得到常量串,再调用OB进行混淆。有兴趣的佬可以搭一个node/django后台实现一下这个方案,用node实现的难度应该是最低的。

var data = "<加密的数据>";
var keywords = CryptoJS.enc.Latin1.parse("B79CD410AF398F7A");
var iv = '';
try {
  // 甚至有环境检测呵呵,node环境会得到错误的iv
  if (top.window.location.href != window.location.href) {
    top.window.location.href = window.location.href;
  }
  iv = CryptoJS.enc.Latin1.parse('E91BF3347F7D1274');
} catch (v44) {
  iv = CryptoJS.enc.Latin1.parse("146385F634C9CB00");
}
var decrypted = CryptoJS.AES.decrypt(data, keywords, {
  'iv': iv,
  'padding': CryptoJS.pad.ZeroPadding
});
var secWords = decrypted.toString(CryptoJS.enc.Utf8).split(',');
var words = new Array(secWords.length);

获取words数组的算法(这段代码是有控制流平坦化的):

for (var i = 0; i < secWords.length; i++) {
  var v50 = secWords[i];
  var v51 = function (v52) {
    var v53 = {
      'NQvuJ': function v54(v55, v56) {
        return v55 % v56;
      },
      'TYKEL': function v57(v58, v59) {
        return v58 - v59;
      },
      'lHuwG': function v60(v61, v62) {
        return v61 - v62;
      }
    };
    return v52 % 2 ? v52 - 2 : v52 - 4;
  };
  var v63 = function (v64) {
    var v65 = {
      'HYHqw': function v66(v67, v68) {
        return v67 + v68;
      },
      'tfwZU': function v69(v70, v71) {
        return v70 * v71;
      },
      'ffVsL': "undefined"
    };
    // 环境检测
    return v64 + 3 * +!(typeof document === "undefined");
  };
  v50 = v51(v50);
  v50 = v63(v50);
  words[i] = String.fromCharCode(v50);
}

至此算法已经很清晰了,可以写exp了。

完美逆向

完美逆向有必要嘛?似乎没有,但自己写一遍这个网站的完美逆向代码,编程能力大概也能有微不足道的提升吧(不能只有我在这件小事上浪费宝贵青春🤬)。算法很简单,但考虑完美破解时一切都会麻烦起来。我们需要解决许多很简单的问题:

  1. python or nodejs?因为只需要发一个请求,且不涉及Cookie的维护,所以我选择了ts-nodetypescriptsuperagent
  2. 如何解析HTML文档?cheerio,API设计特意对标jQuery,体验极佳🐔⌨️🍚
  3. 如何获取data, key, iv?上面人工运行JS代码,获取最终的大数组的做法需要扩展了。
  4. 如何将<span>替换为文本?基于cheerio就很简单:
  const htmlHasOnlyP = inputHTML.replaceAll(/<span.*?<\/span>/g, (span) => {
    const idx = Number((span.match(/\d+/) || [])[0]);
    return words[idx];
  });
  const $ = cheerio.load(htmlHasOnlyP);
  return $('p').text().split('\n').map((txt) => txt.trim()).join(os.EOL);

最后简单说下上述第3点的实现。上一节提到,可以猜测data, key, iv先是使用模板引擎渲染,再调用OB进行混淆。因此我们的过程要与之相反:先使用AST解混淆,再在所有常量串均已恢复的假设前提下,匹配data, key, iv的声明or赋值语句的AST节点,最后获取常量串。

这个过程最麻烦的一步,是解决常量串隐藏。限于编程能力,我写不出一个放之四海而皆准的AST脚本,因此反而可以放开手脚,为每个使用OB的网站进行代码定制,将更多“业务相关”(即只适用于当前网站)的特征用代码表达出来。这个小说网站的常量串隐藏代码样例如下:

var _0xa9e0 = ['JxsFw', 'iksgN', 'qDbwG', 'prototype', 'spzgJ', 'test', 'lo1c0tQyRk7E/Lr2p3puiAKrzgb8Absq4EWawXjoVfP230ItoMvvmsg3H8ccHG1u1qA+T/T4f3Rwi5j40osnuhQGtUj0w5rjN5FglNam4JRHNS126MHWX6+Zk/Aez8M7WttDCxtn6N6/pwWRtVat6vPkvmw9ETifmJ5C94R9hoGnDvNjntiKW6m5HPr+b/j0IvHCUJz8pX4ofi12NyD5aA==', 'enc', 'Latin1', 'parse', 'B79CD410AF398F7A', 'window', 'location', 'href', '146385F634C9CB00', 'ZeroPadding', 'toString', 'Utf8', 'split', 'length', 'createElement', 'style', 'type', 'text/css', 'setAttribute', 'link', 'getElementsByTagName', 'gHLRp', 'CbiRt', 'oKMpY', 'parentNode', 'head', 'appendChild', '4|1|2|5|3|0', 'fromCharCode', 'NQvuJ', 'TYKEL', 'undefined', 'tfwZU', 'ffVsL', 'styleSheets', 'addRule', '.context_kw', '::before', 'content:\x20\x22', 'insertRule', '::before{content:\x20\x22', 'pad', 'clamp', 'sigBytes', 'words', 'BXNBf', 'OMxlD', 'GhFlG'];
(function (_0x149720, _0x36191f) {
  var _0x19a768 = function (_0x5065e2) {
    while (--_0x5065e2) {
      _0x149720['push'](_0x149720['shift']());
    }
  };
  _0x19a768(++_0x36191f);
}(_0xa9e0, 0x1a9));
var _0x0a9e = function (_0x2b4d76, _0x47bf96) {
  _0x2b4d76 = _0x2b4d76 - 0x0;
  var _0x4230d8 = _0xa9e0[_0x2b4d76];
  return _0x4230d8;
};

我们需要做的事情主要有:

  1. 获取偏移量,即上述例子中的0x0
  2. 获取rotate次数,即上述例子中的0x1a9。这是为了获取大数组最终的值。
  3. 获取大数组。
  4. 调用上面已经实现的restoreStringLiteral函数。

这个样例是旧版OB生成的,情况比较简单。我这次实现选择的策略如下:

  1. 获取偏移量函数_0x0a9e的函数体仅识别具有上述3条语句的结构,然后从第1条语句中取出偏移量的值0x0
  2. 通过匹配具有2个参数,且第二个参数是NumericLiteral的自执行函数来获取rotate次数0x1a9
  3. 复用第2点的逻辑,我们认为自执行函数的第一个参数_0xa9e0就是大数组的名称,并通过名称字符串来匹配相应的声明语句。

函数名为autoRestoreStringLiteralViaIIFE,顾名思义,这里我选择的切入点就是自执行函数。代码传送门

import {
  isArrayExpression,
  isBlockStatement,
  isCallExpression,
  isExpressionStatement,
  isFunctionExpression,
  isIdentifier,
  isNumericLiteral,
  isReturnStatement,
  isStringLiteral,
  isVariableDeclaration,
  Node,
  stringLiteral,
  File
} from '@babel/types';
import traverse from '@babel/traverse';
import { strict as assert } from 'assert';
import generator from '@babel/generator';

// 如果常量表不止1处,则此代码不正确
export function restoreStringLiteral (ast: Node, stringLiteralFuncs: string[], getStringArr: (idx: number) => string) {
  // 收集与常量串隐藏有关的变量
  traverse(ast, {
    VariableDeclarator (path) {
      const vaNode = path.node;
      if (!isIdentifier(vaNode.init) || !isIdentifier(vaNode.id)) return;
      if (stringLiteralFuncs.includes(vaNode.init.name)) {
        stringLiteralFuncs.push(vaNode.id.name);
      }
    }
  });
  traverse(ast, {
    CallExpression (path) {
      const cNode = path.node;
      if (!isIdentifier(cNode.callee)) return;
      const varName = cNode.callee.name;
      if (!stringLiteralFuncs.includes(varName)) return;
      const literalNode = cNode.arguments[0];
      if (cNode.arguments.length !== 1 || (!isNumericLiteral(literalNode) && !isStringLiteral(literalNode))) return;
      const idx = Number(literalNode.value);
      path.replaceWith(stringLiteral(getStringArr(idx)));
    }
  });
}

export function rotateArray<T> (a: T[], count: number) {
  count %= a.length;
  return [...a.slice(count), ...a.slice(0, count)];
}

export function autoRestoreStringLiteralViaIIFE (ast: File) {
  let constArrName = '';
  const INITIAL_SHIFT_NUM = -1234567;
  let shiftNum = INITIAL_SHIFT_NUM;
  ast.program.body.findIndex((bodyItem) => {
    if (!isExpressionStatement(bodyItem) ||
        !isCallExpression(bodyItem.expression) ||
        !isFunctionExpression(bodyItem.expression.callee) ||
        bodyItem.expression.arguments.length !== 2) return false;
    const [arg0, arg1] = bodyItem.expression.arguments;
    if (!isIdentifier(arg0) || !isNumericLiteral(arg1)) return false;
    constArrName = arg0.name;
    shiftNum = arg1.value;
    return true;
  });
  assert.ok(constArrName);
  assert.notEqual(shiftNum, INITIAL_SHIFT_NUM);

  let constArrContent: string[] = [];
  let stringHideVarName = '';
  let globalOffset = 0;
  traverse(ast, {
    VariableDeclaration (path) {
      const decl = path.node.declarations[0];
      if (!isIdentifier(decl.id)) return;
      if (decl.id.name === constArrName && isArrayExpression(decl.init)) {
        constArrContent = decl.init.elements.map((item) => {
          assert.ok(isStringLiteral(item));
          return item.value;
        });
      }
      if (isFunctionExpression(decl.init)) {
        if (decl.init.params.length !== 2 ||
            !isBlockStatement(decl.init.body) ||
            decl.init.body.body.length !== 3) return;
        const [s1, s2, s3] = decl.init.body.body;
        if (!isExpressionStatement(s1) ||
            !isVariableDeclaration(s2) ||
            !isReturnStatement(s3)) return;

        path.traverse({
          BinaryExpression (path) {
            assert.ok(isNumericLiteral(path.node.right));
            globalOffset = path.node.right.value;
          }
        });

        const { code } = generator(s2);
        if (!code.includes(constArrName)) return;
        stringHideVarName = decl.id.name;
      }
    }
  });
  constArrContent = rotateArray(constArrContent, shiftNum);

  restoreStringLiteral(ast, [stringHideVarName], (idx: number) => {
    return constArrContent[idx - globalOffset];
  });
}

完整代码:

import {
  isIdentifier,
  isStringLiteral
} from '@babel/types';
import traverse from '@babel/traverse';
import CryptoJS from 'crypto-js';
import superagent from 'superagent';
import * as cheerio from 'cheerio';
import * as parser from '@babel/parser';
import fs from 'fs';
import { strict as assert } from 'assert';
import path from 'path';
import { autoRestoreStringLiteralViaIIFE } from '../restoreStringLiteral';
import os from 'os';
import { getLegalFileName } from '../file_utils';

export function parseHTML (inputHTML: string, data: string, keywordsEnc: string, ivEnc: string) {
  const keywords = CryptoJS.enc.Latin1.parse(keywordsEnc);
  const iv = CryptoJS.enc.Latin1.parse(ivEnc);
  const decrypted = CryptoJS.AES.decrypt(data, keywords, {
    iv,
    padding: CryptoJS.pad.ZeroPadding
  });
  const secWords = decrypted.toString(CryptoJS.enc.Utf8).split(',');
  // console.log('secWords', secWords); // dbg
  const words: string[] = [];
  for (let i = 0; i < secWords.length; i++) {
    let v50 = Number(secWords[i]);
    const v51 = function (v52: number) {
      return v52 % 2 ? v52 - 2 : v52 - 4;
    };
    const v63 = function (v64: number) {
      return v64 + 3 * +true;
    };
    v50 = v51(v50);
    v50 = v63(v50);
    words[i] = String.fromCharCode(v50);
  }
  // console.log(words); // dbg

  const htmlHasOnlyP = inputHTML.replaceAll(/<span.*?<\/span>/g, (span) => {
    const idx = Number((span.match(/\d+/) || [])[0]);
    return words[idx];
  });
  const $ = cheerio.load(htmlHasOnlyP);
  return $('p').text().split('\n').map((txt) => txt.trim()).join(os.EOL);
}

export function getDataFromJSCode (jsCode: string) {
  const ast = parser.parse(jsCode);

  autoRestoreStringLiteralViaIIFE(ast);

  let data = '';
  let keywordsEnc = '';
  let ivEnc = '';
  traverse(ast, {
    VariableDeclarator (path) {
      const idNode = path.node.id;
      if (!isIdentifier(idNode)) return;
      if (idNode.name === 'keywords') {
        path.traverse({
          CallExpression (path) {
            const args = path.node.arguments;
            assert.equal(args.length, 1);
            assert.ok(isStringLiteral(args[0]));
            keywordsEnc = args[0].value;
          }
        });
      }
      if (idNode.name === 'data') {
        assert.ok(isStringLiteral(path.node.init));
        data = path.node.init.value;
      }
    },
    AssignmentExpression (path) {
      if (!isIdentifier(path.node.left) || path.node.left.name !== 'iv' || ivEnc) return;
      path.traverse({
        CallExpression (path) {
          const args = path.node.arguments;
          assert.equal(args.length, 1);
          assert.ok(isStringLiteral(args[0]));
          ivEnc = args[0].value;
        }
      });
    }
  });
  return {
    data, keywordsEnc, ivEnc
  };
}

export function getDataFromHTML ($: cheerio.CheerioAPI) {
  const scriptTags = $('script');
  let data = '', keywordsEnc = '', ivEnc = '';
  scriptTags.each((i, el) => {
    const jsCode = $(el).text();
    if (!jsCode.includes('CryptoJS')) return true;
    const res = getDataFromJSCode(jsCode);
    data = res.data;
    keywordsEnc = res.keywordsEnc;
    ivEnc = res.ivEnc;
    return false;
  });
  return {
    data, keywordsEnc, ivEnc
  };
}

export function getInputHTML (text: string) {
  const $ = cheerio.load(text);
  return { $, inputHTML: $('.rdtext').html() || '' };
}

function main () {
  const novelURLs = [
    // html列表,简单的逐个遍历
  ];
  const headers = {
    'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',
    'accept-language': 'zh-CN,zh;q=0.9',
    'cache-control': 'no-cache',
    'pragma': 'no-cache',
    'sec-ch-ua': '"Not_A Brand";v="99", "Google Chrome";v="109", "Chromium";v="109"',
    'sec-ch-ua-mobile': '?0',
    'sec-ch-ua-platform': '"Windows"',
    'sec-fetch-dest': 'document',
    'sec-fetch-mode': 'navigate',
    'sec-fetch-site': 'same-origin',
    'sec-fetch-user': '?1',
    'upgrade-insecure-requests': '1',
    'Referer': '<referer>',
    'Referrer-Policy': 'strict-origin-when-cross-origin'
  };
  novelURLs.forEach(async (novelURL) => {
    const resp = await superagent.get(novelURL).set(headers);
    console.log('resp.text', resp.text.substring(0, 100)); // dbg
    const { $, inputHTML } = getInputHTML(resp.text);
    const { data, keywordsEnc, ivEnc } = getDataFromHTML($);
    const resultNovelText = parseHTML(inputHTML, data, keywordsEnc, ivEnc);
    console.log('resultNovelText', resultNovelText); // dbg
    const fileName = path.resolve('src', 'outputs', getLegalFileName(
      `${$('title').html()}.txt`,
      `${novelURL.substring(novelURL.lastIndexOf('/') + 1)}.txt`
    ));
    fs.writeFileSync(fileName, resultNovelText, 'utf-8');
  });
}

if (require.main === module) {
  main();
}

免费评分

参与人数 87吾爱币 +88 热心值 +75 收起 理由
y123123 + 1 + 1 谢谢@Thanks!
tyf147258 + 1 + 1 谢谢@Thanks!
drivelin + 1 + 1 我很赞同!
jr928110456 + 1 + 1 非常详细非常详尽配置
wsk00432 + 1 + 1 谢谢@Thanks!
xiaogueiee1 + 1 谢谢@Thanks!
b12312312 + 1 + 1 我很赞同!
X54214 + 1 + 1 用心讨论,共获提升!
EWq0 + 1 已经处理,感谢您对吾爱破解论坛的支持!
rtc + 1 + 1 我很赞同!
东风破解 + 1 热心回复!
Boly + 1 谢谢@Thanks!
kanou1993 + 1 + 1 用心讨论,共获提升!
zozoz + 1 + 1 我很赞同!
笙若 + 1 + 1 谢谢@Thanks!
xwa3358258 + 1 热心回复!
l07001016 + 1 我很赞同!
monicx + 1 + 1 谢谢@Thanks!
mqliu + 1 我很赞同!
iNIC + 1 + 1 谢谢@Thanks!
o0呆o呆0o + 1 + 1 鼓励转贴优秀软件安全工具和文档!
cnjn + 1 热心回复!
moyes + 1 欢迎分析讨论交流,吾爱破解论坛有你更精彩!
岚东 + 1 + 1 谢谢@Thanks!
xcngg + 1 + 1 我很赞同!
ytfh1131 + 1 + 1 谢谢@Thanks!
1MajorTom1 + 1 热心回复!
yc2023 + 1 + 1 热心回复!
aabbcc123123 + 1 + 1 谢谢@Thanks!
byinto + 1 谢谢@Thanks!
wzg020228 + 1 + 1 谢谢@Thanks!
onlywey + 1 + 1 谢谢@Thanks!
Diamondzl + 1 + 1 我很赞同!
magicmen + 1 + 1 谢谢@Thanks!
xtkhnl2023 + 1 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
b12312312 + 1 + 1 热心回复!
3869742 + 1 + 1 我很赞同!
kanyanhao + 1 + 1 我很赞同!
zhankong123 + 1 + 1 谢谢@Thanks!
NebulaKikyo + 1 + 1 谢谢@Thanks!
feichen0921 + 1 + 1 谢谢@Thanks!
starcrafter + 1 + 1 谢谢@Thanks!
lyj722 + 1 + 1 谢谢@Thanks!
ouyang12138 + 1 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
爱你个蛇皮怪 + 1 谢谢@Thanks!
ezerear + 1 + 1 谢谢@Thanks!
cloudy520 + 1 + 1 谢谢@Thanks!
sciencevessel + 1 + 1 用心讨论,共获提升!
Kanchow + 1 + 1 热心回复!
AkaTerrorist + 1 + 1 我很赞同!
nimo + 1 + 1 谢谢@Thanks!
RS水果 + 10 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
N1san + 1 + 1 热心回复!
anoetl + 1 我很赞同!
gqdsc + 1 谢谢@Thanks!
muyan1995 + 1 我很赞同!
fengbolee + 2 + 1 欢迎分析讨论交流,吾爱破解论坛有你更精彩!
wizarrr + 1 + 1 谢谢@Thanks!
featmellwo + 2 + 1 我很赞同!
yxpp + 1 谢谢@Thanks!
wanjingbo + 1 谢谢@Thanks!
photor + 1 + 1 用心讨论,共获提升!
wjy52125 + 1 + 1 欢迎分析讨论交流,吾爱破解论坛有你更精彩!
owouwu + 1 + 1 用心讨论,共获提升!
mrxxs + 1 + 1 谢谢@Thanks!
sakura99 + 1 谢谢@Thanks!
l0rraine + 1 + 1 大佬牛啤,吾爱破解论坛因你更精彩!
Imba77 + 1 谢谢@Thanks!
dialga + 1 + 1 谢谢@Thanks!
landseer2008 + 1 + 1 谢谢@Thanks!
GZDCS + 1 我很赞同!
laoda2128 + 1 + 1 谢谢@Thanks!
diy520zhu + 1 + 1 我很赞同!
M79241 + 1 + 1 热心回复!
sabirjan2023 + 1 + 1 谢谢@Thanks!
Jackyyy + 1 + 1 谢谢@Thanks!
woyucheng + 1 + 1 谢谢@Thanks!
yjn866y + 1 + 1 谢谢@Thanks!
skiss + 1 + 1 谢谢@Thanks!
xuantoo + 1 + 1 鼓励转贴优秀软件安全工具和文档!
HillBoom + 1 + 1 用心讨论,共获提升!
fxzh007 + 1 + 1 用心讨论,共获提升!
daoye9988 + 1 + 1 谢谢@Thanks!
Rangon + 1 热心回复!
kimchow + 1 + 1 用心讨论,共获提升!
为之奈何? + 1 + 1 我很赞同!
zhangxiaoxiao + 1 谢谢@Thanks!

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

推荐
塞北的雪 发表于 2023-4-11 11:33
本帖最后由 塞北的雪 于 2023-4-11 11:50 编辑

根据楼主的程序,写了个js脚本,可以直接注入页面执行
[JavaScript] 纯文本查看 复制代码
css=document.styleSheets[0].cssRules;Object.keys(css).forEach(function(k,v){
        stylename=css[k].selectorText.split('::')[0];
        content=css[k].style.content.replaceAll('"','');
        document.querySelectorAll(stylename).forEach(function(ek,ev){
                ek.replaceWith(content);
        });
});


油猴脚本:https://greasyfork.org/zh-CN/scripts/463724

微信截图_20230411113321.png (213.18 KB, 下载次数: 23)

微信截图_20230411113321.png

免费评分

参与人数 8吾爱币 +20 热心值 +8 收起 理由
hex1129 + 1 + 1 我很赞同!
adolphin + 1 + 1 牛, 上面没看懂下面的能直接用
aabbcc123123 + 1 + 1 我很赞同!
soyiC + 1 + 1 欢迎分析讨论交流,吾爱破解论坛有你更精彩!
qq87700906 + 1 + 1 用心讨论,共获提升!
coderyl + 1 + 1 我很赞同!
RS水果 + 10 + 1 非常不错,我一开始还想着用js截屏,OCR识别,这方法太牛了!
涛之雨 + 4 + 1 我很赞同!

查看全部评分

推荐
35925 发表于 2023-4-11 07:57
3#
111wdw 发表于 2023-4-11 08:18
4#
daoye9988 发表于 2023-4-11 08:18
感谢感谢,爬取经验
5#
blindcat 发表于 2023-4-11 08:25
学习了,感谢分享
6#
yizhimei123 发表于 2023-4-11 08:26
很好的教程,感谢分享!
7#
Klock0828 发表于 2023-4-11 08:30
我对逆向很感兴趣,感谢大佬分享项目,有学习到!
8#
qq734330359 发表于 2023-4-11 08:39
学习学习
9#
Easonll 发表于 2023-4-11 08:54
感谢分享,可以啊大兄弟
10#
istat 发表于 2023-4-11 08:54
谢谢分享。
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - LCG - LSG ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2024-12-21 19:33

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

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