首页 » 技术分享 » 反爬虫破解——破解腾讯动漫js加密

反爬虫破解——破解腾讯动漫js加密

 

以下内容仅用于技术交流,严禁用于恶意下载腾讯动漫资源

最近在研究破解反爬虫机制,正好发现腾讯动漫的js反爬机制有点意思,这里就就以此为例介绍一下如何破解反爬虫

思路分析

首先我们随便打开一个漫画,看看里面的内容,以 中国惊奇先生 为例,打开控制台(网站上禁止右键,因此可以使用快捷键 alt + commond + i 打开控制台)选择network面板,可以看到如下数据包

在这里插入图片描述
查看 https://ac.qq.com/ComicView/index/id/511915/cid/43 请求的response可以看到这里是返回了一个html,而最左边我框柱的请求: https://manhua.qpic.cn/manhua_detail/0/23_09_41_a6e25b26a841828110792db68802b052a_113865916.jpg/0 就是图片链接;而在两个请求中间只加载了一个css,并没有ajax请求来获取图片链接,因此我们查找一下图片地址看看html中是否返回了图片链接,由于考虑到可能存在url拼接的情况,这里我截取中间部分进行全局搜索
在这里插入图片描述
可以发现在咱们访问的原始页面中有此图片的链接,但并没有看到后续图片的链接,于是我试着在网页上往下滑动加载更多的图片,然后再看下控制台
在这里插入图片描述
可以发现直接开始加载图片了,中间并没有发送ajax请求获取新的图片地址。然后搜索一下新加载的图片链接,也没有发现哪里能找到,因此推测图片链接做了js加密。

我们先分析一下页面返回的Html内容,大致浏览一下结构会发现这部分极其扎眼
在这里插入图片描述
可能DATA属性就是图片加密后的内容,我们全局搜索一下"DATA"(注意区分大小写)
在这里插入图片描述
可以看到DATA只存在于https://ac.gtimg.com/media/js/ac.page.chapter.view_v2.6.0.js?v=20200914 中。我们点击"pretty-print"美化一下,然后把每个用到"DATA"的地方看一下,可以看到在这个js中"DATA"出现了37次,但只有一次——也就是我上图中的地方——是给DATA赋值的,那么我们重点研究这里。先我们再全局搜一下"_v",可以发现_v只在当前js出现,且多为取值操作,唯一一次意义不明的情况在"var DATA=_v"的上一行
在这里插入图片描述
可以看到这里是一个加密的js方法(这里需要熟悉js常用语法,爬虫需要的知识面很广呀!!!)

eval(function(p, a, c, k, e, r) {
            e = function(c) {
                return (c < a ? "" : e(parseInt(c / a))) + ((c = c % a) > 35 ? String.fromCharCode(c + 29) : c.toString(36))
            }
            ;
            if (!"".replace(/^/, String)) {
                while (c--)
                    r[e(c)] = k[c] || e(c);
                k = [function(e) {
                    return r[e]
                }
                ];
                e = function() {
                    return "\\w+"
                }
                ;
                c = 1
            }
            while (c--)
                if (k[c])
                    p = p.replace(new RegExp("\\b" + e(c) + "\\b","g"), k[c]);
            return p
        }("p y(){i=\"J+/=\";O.D=p(c){s a=\"\",b,d,h,f,g,e=0;C(c=c.z(/[^A-G-H-9\\+\\/\\=]/g,\"\");e<c.k;)b=i.l(c.m(e++)),d=i.l(c.m(e++)),f=i.l(c.m(e++)),g=i.l(c.m(e++)),b=b<<2|d>>4,d=(d&15)<<4|f>>2,h=(f&3)<<6|g,a+=7.5(b),w!=f&&(a+=7.5(d)),w!=g&&(a+=7.5(h));v a=u(a)};u=p(c){C(s a=\"\",b=0,d=17=8=0;b<c.k;)d=c.o(b),Q>d?(a+=7.5(d),b++):R<d&&S>d?(8=c.o(b+1),a+=7.5((d&F)<<6|8&r),b+=2):(8=c.o(b+1),x=c.o(b+2),a+=7.5((d&15)<<12|(8&r)<<6|x&r),b+=3);v a}}s B=I y(),T=W['K'+'L'].M(''),N=W['n'+'P'+'e'],j,t,q;N=N.U(/\\d+[a-V-Z]+/g);j=N.k;X(j--){t=Y(N[j])&10;q=N[j].z(/\\d+/g,'');T.11(t,q.k)}T=T.13('');14=16.E(B.D(T));", 62, 70, "|||||fromCharCode||String|c2||||||||||_keyStr|len|length|indexOf|charAt||charCodeAt|function|str|63|var|locate|_utf8_decode|return|64|c3|Base|replace|||for|decode|parse|31|Za|z0|new|ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789|DA|TA|split||this|onc|128|191|224||match|zA||while|parseInt||255|splice||join|_v||JSON|c1".split("|"), 0, {}))
    

我们随便找个网站进行解密(在网站上搜索 “js eval 解密”即可),这里我用的是 beautifyconverter,解密之后的js如下:

function Base() {
    _keyStr = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
    this.decode = function(c) {
        var a = "",
            b, d, h, f, g, e = 0;
        for (c = c.replace(/[^A-Za-z0-9\+\/\=]/g, ""); e < c.length;) b = _keyStr.indexOf(c.charAt(e++)), d = _keyStr.indexOf(c.charAt(e++)), f = _keyStr.indexOf(c.charAt(e++)), g = _keyStr.indexOf(c.charAt(e++)), b = b << 2 | d >> 4, d = (d & 15) << 4 | f >> 2, h = (f & 3) << 6 | g, a += String.fromCharCode(b), 64 != f && (a += String.fromCharCode(d)), 64 != g && (a += String.fromCharCode(h));
        return a = _utf8_decode(a)
    };
    _utf8_decode = function(c) {
        for (var a = "", b = 0, d = c1 = c2 = 0; b < c.length;) d = c.charCodeAt(b), 128 > d ? (a += String.fromCharCode(d), b++) : 191 < d && 224 > d ? (c2 = c.charCodeAt(b + 1), a += String.fromCharCode((d & 31) << 6 | c2 & 63), b += 2) : (c2 = c.charCodeAt(b + 1), c3 = c.charCodeAt(b + 2), a += String.fromCharCode((d & 15) << 12 | (c2 & 63) << 6 | c3 & 63), b += 3);
        return a
    }
}
var B = new Base(),
    T = W['DA' + 'TA'].split(''),
    N = W['n' + 'onc' + 'e'],
    len, locate, str;
N = N.match(/\d+[a-zA-Z]+/g);
len = N.length;
while (len--) {
    locate = parseInt(N[len]) & 255;
    str = N[len].replace(/\d+/g, '');
    T.splice(locate, str.length)
}
T = T.join('');
_v = JSON.parse(B.decode(T));

分析这段js可以知道大致逻辑为根据W[‘DATA’]、W[nonce]经过计算得到_v。而我们通过debug可以知道_v中包含了所有的图片链接
在这里插入图片描述
ok,现在我们的目标变成了找到W[‘DATA’]和W[nonce],DATA我们上面已经找到了,就在页面返回的html中,现在我们全局搜索一下"nonce"
在这里插入图片描述
会发现只有一个结果,ok,我们把上面解密后的js中的W[DATA]和W[nonce]替换一下,然后在控制台中运行一下
可以发现运行会直接报错
在这里插入图片描述
这说明这个参数肯定有问题。然后想到在解密后的JS中使用W[‘DA’ + ‘TA’]代替W[‘DATA’]、W[‘n’ + ‘onc’ + ‘e’]代替W[‘nonce’],那么会不会DATA和nonce其实也是用拼接的方式得到的呢,我们分别对nonce进行分割然后搜索,可以发现有两处十分可疑
在这里插入图片描述

在这里插入图片描述
我们分别把这两个值填充到解密后的js中执行一下
在这里插入图片描述
在这里插入图片描述
很明显可以知道咱们的第二段js就是咱们的目标,ok,到这里我们的解密工作已经完成。

爬取思路

爬取流程

  1. 访问某个漫画的地址,如https://ac.qq.com/Comic/comicInfo/id/511915,获得所有章节列表
  2. 遍历章节列表,如https://ac.qq.com/ComicView/index/id/511915/cid/43,从其响应中获得DATA,与nonce
  3. 用上面解密后的js对这DATA与nonce进行计算的到当前章节所有图片的额链接
  4. 保存图片

Coding

下面让我们来写代码吧

  1. 首先我们访问漫画地址获得所有章节
	/**
     * @param url 漫画Url
     * @return 每个章节的地址
     * @throws Exception
     */
    private List<String> getChapterUrls(String url) throws Exception {
        String s = sendRequest(url);
        Document document = Jsoup.parse(s);
        Elements hrefs = document.getElementsByAttributeValueMatching("href", "/ComicView/index/id/");
        List<String> chapterUrls = hrefs.stream().map(element -> URL_PREFIX + element.attr("href")).collect(Collectors.toList());
        return chapterUrls;
    }
  1. 然后遍历访问每个章节的Url,从响应中获得DATA和nonce
/**
     * @param htmlContent 章节url的html文本
     * @return TencentChapter 每章内容信息
     */
    private TencentChapter getPics(String htmlContent) {
        Document document = Jsoup.parse(htmlContent);
        Elements scripts = document.getElementsByTag("script");
        String data = "";
        String nonce = "";
        for (Element script : scripts) {
            //获取DATA
            if (StringUtils.contains(script.html(), "DATA")) {
                data = StringUtils.substringBetween(script.html(), "'", "'");
            }
            //获取nonce
            if (StringUtils.containsAny(script.html(), "window\\[.*?\\].*?eval")) {
                String str = StringUtils.substringBefore(script.html(), " = ");
                str = StringUtils.replace(str, "\"+\"", "");
                if (StringUtils.equalsIgnoreCase(str, "window[\"nonce\"]")) {
                    nonce = StringUtils.substringBetween(script.html(), "] = ", ";");
                    nonce = StringUtils.replace(nonce, "!!document.getElementsByTagName('html')", "true");
                    nonce = StringUtils.replace(nonce, "(!window.Array)", "false");
                    nonce = StringUtils.replace(nonce, "!!document.children", "true");
                    nonce = executeJS(nonce);
                }

            }
        }

        if (StringUtils.isAnyBlank(data, nonce)) {
//            log.error("data or nonce get failed,data:{},nonce:{}", data, nonce);
            return null;
        }
		
		//通过data和nonce计算出网站返回的json(其中包含图片)
        String jsonData = jsDecode(data, nonce);

        JSONObject sourceJson = JSON.parseObject(jsonData);
        String title = sourceJson.getJSONObject("comic").getString("title");
        String chapterSeq = sourceJson.getJSONObject("chapter").getString("cSeq");
        String chapterName = sourceJson.getJSONObject("chapter").getString("cTitle");
        JSONArray pictures = sourceJson.getJSONArray("picture");
        List<String> urls = pictures.stream().map(j -> ((JSONObject) j).getString("url")).collect(Collectors.toList());

		//创建一个对象用户保存漫画相关信息
        TencentChapter chapter = new TencentChapter();
        chapter.setTitle(title);
        chapter.setChapterName(chapterName);
        chapter.setChapterSeq(chapterSeq);
        chapter.setPics(urls);
        return chapter;
    }

ps.需要注意,由于nonce的值是一段js,需要执行之后才能得到结果,因此我们需要在java中执行才能获取到真实值。又由于nonce的计算可能需要使用到window、document等对象,而java执行js的时候是没有这些对象的,因此我在上面的代码中对一些值进行了替换。executJS方法如下

   private String executeJS(String jsContent) {
        try {
            ScriptEngineManager manager = new ScriptEngineManager();
            ScriptEngine engine = manager.getEngineByName("JavaScript");
            CompiledScript script = ((Compilable) engine).compile(jsContent);
            Object object = script.eval();
            return object.toString();
        } catch (Exception e) {
            LOGGER.error("jsContent:{}", jsContent);
            e.printStackTrace();
        }
        return "";
    }
  1. 把解密后的js转化为java语言
    private String jsDecode(String data, String nonce) {
        List<String> dataList = converter(data);

        List<String> nonceList = matchNonce(nonce);
        for (int i = nonceList.size() - 1; i >= 0; i--) {
            String non = nonceList.get(i);
            int locate = getNumbers(non) & 255;
            String str = non.replaceAll("\\d+", "");
            dataList.subList(locate, locate + str.length()).clear();
        }


        data = dataList.stream().collect(Collectors.joining(""));
        String decode = decode(data);
        return decode;
    }

    private String decode(String source) {
        source = source.replaceAll("[^A-Za-z0-9\\+\\/\\=]", "");
        String keyStr = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
        String a = "";
        int b, d, h, f, g, e = 0;

        for (int i = 0; i < source.length() && e < source.length(); i++) {
            b = keyStr.indexOf(source.charAt(e));
            e++;
            d = keyStr.indexOf(source.charAt(e));
            e++;
            f = keyStr.indexOf(source.charAt(e));
            e++;
            g = keyStr.indexOf(source.charAt(e));
            e++;

            b = b << 2 | d >> 4;
            d = (d & 15) << 4 | f >> 2;
            h = (f & 3) << 6 | g;

            a += (char) b;
            if (64 != f) {
                a += (char) d;
            }
            if (64 != g) {
                a += (char) h;
            }
        }
        return a;
    }

    private List<String> matchNonce(String nonce) {
        List<String> allMatches = new ArrayList<>();
        Matcher m = Pattern.compile("\\d+[a-zA-Z]+").matcher(nonce);
        while (m.find()) {
            allMatches.add(m.group());
        }
        return allMatches;
    }

    private int getNumbers(String s) {
        String[] n = s.split(""); //array of strings
        StringBuffer f = new StringBuffer(); // buffer to store numbers
        for (int i = 0; i < n.length; i++) {
            if ((n[i].matches("[0-9]+"))) {// validating numbers
                f.append(n[i]); //appending
            } else {
                //parsing to int and returning value
                return Integer.parseInt(f.toString());
            }
        }


        return 0;
    }


    private List<String> converter(String data) {
        char[] chars = data.toCharArray();
        List<String> result = new ArrayList<>();
        for (char aChar : chars) {
            result.add(Character.toString(aChar));
        }
        return result;
    }
  1. 下载保存相关图片
@Test
    public void test() throws Exception {
        //通过漫画获取总章节数
        List<String> chapterUrls = getChapterUrls(CARTOOON_URL);
        for (String chapterUrl : chapterUrls) {
            String htmlContent = sendRequest(chapterUrl);
            try {
                TencentChapter tencentChapter = getPics(htmlContent);
                if (tencentChapter == null) {
                    continue;
                }
                int i = 1;
                for (String pic : tencentChapter.getPics()) {
                    File cartoonBaseDir = new File(DIR_PATH + tencentChapter.getTitle());
                    if (!cartoonBaseDir.exists()) {
                        cartoonBaseDir.mkdir();
                    }
                    File chapterDir = new File(DIR_PATH + tencentChapter.getTitle() + "/" + tencentChapter.getChapterSeq() + "-" + tencentChapter.getChapterName());
                    if (!chapterDir.exists()) {
                        chapterDir.mkdir();
                    }
                    String imagePath = chapterDir.getPath() + "/" + i++ + ".jpg";
                    File imageFile = new File(imagePath);
                    if(imageFile.exists()){
                        continue;
                    }
                    downloadFileFromUrl(pic, imagePath);
                }
                LOGGER.info("chapter :{} download sucessful", tencentChapter.getChapterName());
            } catch (Exception e) {
                LOGGER.error("something error:{}", e.getMessage());
            }

        }
    }

ok,到这这里整个代码就完成了,可以下载整本漫画,并分章节按目录保存。后续还会分享更多的反爬虫破解技术,如果大家有什么加密很有趣的网站也可以让我研究一下呀

转载自原文链接, 如需删除请联系管理员。

原文链接:反爬虫破解——破解腾讯动漫js加密,转载请注明来源!

0