【JS逆向】某茄小说逆向分析

查看 73|回复 9
作者:littlewhite11   
逆向目标
  • 网址:aHR0cHM6Ly9mYW5xaWVub3ZlbC5jb20vcmVhZGVyLzczOTIxNjQxOTgzNzA3ODc4NjU=
  • 目标:成功拿到文章内容

    抓包分析
    首先点击下一章,发起一个请求,本篇文章主要讲字体反爬,查询参数是什么就不过多说了,某大厂比较热门的一个参数。该参数逆向的话,总的来说还是一个vmp,找到函数调用的地方插桩,看日志,然后配合条件断点,耐心细心,最后一定可以弄出来的。


    1.png (24.08 KB, 下载次数: 3)
    下载附件
    2024-11-30 12:36 上传

    我们看响应,一大堆乱码,有很大的概率是字体反爬。


    2.png (81.59 KB, 下载次数: 3)
    下载附件
    2024-11-30 12:36 上传

    进一步验证一下,有字体文件。


    3.png (26.46 KB, 下载次数: 3)
    下载附件
    2024-11-30 12:36 上传

    有CSS样式,字体反爬没错了。


    4.png (23.8 KB, 下载次数: 0)
    下载附件
    2024-11-30 12:36 上传

    因为不常遇到字体反爬,所以这方面的经验相对较少。
    我简单说下我的处理方式,请各位路过的大佬指导指导小弟。

    首先观察字体反爬的类型,雪碧图?样式偏移?还是其他的...
    其次看是不是动态字体,就是数据接口会不会连带有和字体相关的请求
    最后就是字体解析,文件类型的话直接拿映射表,雪碧图和样式偏移就找对应的偏移量...

    逆向分析
    根据前面分析的流程,我们知道dc027189e0ba4cd大概率就是字体文件。


    5.png (21.96 KB, 下载次数: 0)
    下载附件
    2024-11-30 12:36 上传

    我们可以直接双击下载字体文件,然后找一个网站进行解析。
    字体文件解析网址:aHR0cHM6Ly9rZWtlZTAwMC5naXRodWIuaW8vZm9udGVkaXRvci8=。
    很不给面子,说错误的ttf文件(网上说可能woff2是较新的格式,然后网站不支持解析这种类型的字体文件)。


    6.png (14.62 KB, 下载次数: 0)
    下载附件
    2024-11-30 12:36 上传

    那我们就模拟请求下载字体文件,经过了一番尝试,可以下载otf类型的文件并完成解析,我是直接把文件后缀改成了otf,可能服务器并没有校验文件的类型,如果有大佬试出了其他类型的,或者有其他方法的,也可以分享分享。


    7.png (53.42 KB, 下载次数: 0)
    下载附件
    2024-11-30 12:36 上传

    python代码:
    import requests
    headers = {
        "accept": "application/font-otf",
        "accept-language": "zh-CN,zh;q=0.9,en;q=0.8,pt;q=0.7",
        "cache-control": "no-cache",
        "origin": "脱敏信息",
        "pragma": "no-cache",
        "priority": "u=0",
        "referer": "脱敏信息",
        "sec-ch-ua": "\"Google Chrome\";v=\"131\", \"Chromium\";v=\"131\", \"Not_A Brand\";v=\"24\"",
        "sec-ch-ua-mobile": "?0",
        "sec-ch-ua-platform": "\"Windows\"",
        "sec-fetch-dest": "font",
        "sec-fetch-mode": "cors",
        "sec-fetch-site": "cross-site",
        "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"
    }
    url = "脱敏信息/dc027189e0ba4cd.otf"
    response = requests.get(url, headers=headers)
    with open('test.otf', 'wb') as f:
        print(response.content)
        f.write(response.content)
    然后我们回到网站,再分析分析响应的内容,
    我们随便选一个文字,就“人”这个字吧,响应中是不可见字符,仔细观察网页上的,和正常的文字也会有点区别。


    8.png (65.16 KB, 下载次数: 0)
    下载附件
    2024-11-30 12:36 上传

    虽然这是不可见字符,但我们可以得到它的unicode码点,且这个码点大概率是在字体文件有对应关系的。
    可以在下图这复制,然后去控制台拿到这个不可见字符的unicode码点,' '.charCodeAt()得到58562。


    9.png (20.77 KB, 下载次数: 0)
    下载附件
    2024-11-30 12:36 上传

    然后我们回到字体解析网站找“人”这个字,发现确实对应了gid58562,gid是标识字形的唯一标识符。


    10.png (25.14 KB, 下载次数: 0)
    下载附件
    2024-11-30 12:36 上传

    按照往常的经验,标准的字体文件每个字的唯一标识符gid会对应自己的unicode编码,
    我们可以看看“人”这个字的unicode编码是多少,发现是20154,对不上,那这字体文件应该就是自定义的,也就是说我们不能通过字体文件中的唯一标识符知道是什么字。


    11.png (6.08 KB, 下载次数: 0)
    下载附件
    2024-11-30 12:36 上传

    那我们该怎么处理才能拿到文字和唯一标识符gid的对应关系呢,我瞎摸索出了两种比较鸡肋的方法,都是基于svg转图片然后进行文字识别的。
    第一种
    半自动化方案,有点鸡肋,运行程序后需要手动加载字体文件,然后控制台回车就可以。
    这种方案只适合非动态字体,只是减少工作量。
    import time
    import cairosvg
    from DrissionPage import ChromiumPage
    from lxml import etree
    import os
    class FontSpider:
        # 图片文件夹路径
        if not os.path.exists('./test'):
            os.mkdir('./test')
        @staticmethod
        def svg_to_image(html_code):
            tree = etree.HTML(html_code)
            svg_lst = tree.xpath('//*[@class="glyf-list"]/div/svg')
            unicode_lst = tree.xpath('//*[@class="glyf-list"]/div/div[last()]/text()')
            for svg, unicode in zip(svg_lst, unicode_lst):
                if unicode.startswith('.'):
                    continue
                svg.set('width', '1000')
                svg.set('height', '1000')
                svg_str = etree.tostring(svg, encoding='unicode', pretty_print=True)
                print(svg_str)
                # 将SVG字符串转换为PNG文件
                cairosvg.svg2png(bytestring=svg_str, write_to=f'./test/{unicode.replace("gid", "")}.png', background_color='white')
        @staticmethod
        def get_single_char_image():
            driver = ChromiumPage()
            driver.get("https://kekee000.github.io/fonteditor/")
            input('加载字体文件完毕后回车>>>')
            FontSpider.svg_to_image(driver.html)
            for i in range(3):
                driver.ele('xpath://*[@id="glyf-list-pager"]/button[3]').click()
                time.sleep(1)
                FontSpider.svg_to_image(driver.html)
            driver.quit()
    if __name__ == '__main__':
        FontSpider.get_single_char_image()
    第二种
    这种方案直接将字体文件解析为svg然后转为png图片。
    from fontTools import ttLib
    from fontTools.pens.svgPathPen import SVGPathPen
    import pathlib
    import cairosvg
    class FontConverter:
        def __init__(self, font_path):
            self.font = ttLib.TTFont(font_path)
            self.units_per_em = self.font['head'].unitsPerEm
        def get_glyph_names(self):
            return self.font.getGlyphOrder()
        def get_cmap(self):
            return self.font.getBestCmap()
        def glyph_to_svg(self, glyph_name):
            # 获取字形对象
            glyph_set = self.font.getGlyphSet()
            glyph = glyph_set[glyph_name]
            # 创建SVG路径笔
            pen = SVGPathPen(glyph_set)
            # 绘制字形
            glyph.draw(pen)
            # 获取路径数据
            path_data = pen.getCommands()
            # 获取边界框
            bbox = None
            if hasattr(glyph, 'xMin'):
                bbox = {
                    'xMin': glyph.xMin,
                    'yMin': glyph.yMin,
                    'xMax': glyph.xMax,
                    'yMax': glyph.yMax
                }
            return {
                'path': path_data,
                'bbox': bbox
            }
        def create_svg(self, glyph_name, width=1000, height=1000):
            glyph_data = self.glyph_to_svg(glyph_name)
            # 计算变换参数
            scale = 0.9  # 缩放因子
            baseline = self.units_per_em * 0.8  # 基线位置
            svg = f'''
                
                   
                
            '''
            return svg
        def batch_convert(self, image_folder):
            output_path = pathlib.Path(image_folder)
            output_path.mkdir(parents=True, exist_ok=True)
            # 获取字符映射
            cmap = self.get_cmap()
            # 转换每个字形
            for unicode_value, glyph_name in cmap.items():
                try:
                    # 创建SVG
                    svg = self.create_svg(glyph_name)
                    # 将SVG字符串转换为PNG文件
                    cairosvg.svg2png(
                        bytestring=svg,
                        write_to=f'{image_folder}/{unicode_value}.png',
                        background_color='white',
                        output_height=64,
                        output_width=64
                    )
                except Exception as e:
                    print(f"转换字形 {glyph_name} (U+{unicode_value:04X}) 时出错: {e}")
    def main(fp, image_folder):
        # 使用示例
        converter = FontConverter(fp)
        # 获取所有字形名称
        glyph_names = converter.get_glyph_names()
        print(f"字体包含 {len(glyph_names)} 个字形")
        # 批量转换
        converter.batch_convert(image_folder)
    if __name__ == '__main__':
        font_path = './font.otf'
        image_folder = 'test'
        main(font_path, image_folder)
    文件名都是每个字的唯一标识符gid。
    然后我们需要进行文字识别来构造一个映射,键为gid,值就是哪一个字。
    识别用的开源库ddddocr,我们将识别结果保存为一个json文件方便后续使用。
    import os
    import ddddocr
    from pathlib import Path
    import json
    from PIL import Image
    import io
    import matplotlib.pyplot as plt
    ocr = ddddocr.DdddOcr(show_ad=False, beta=True, use_gpu=True)
    # 图片保存的文件夹路径
    image_folder = 'test'
    dir_path = Path(image_folder)
    files = list(dir_path.glob('*.png'))
    with open('mapping.json', 'w', encoding='utf-8') as mf:
        data = {}
        for file in files:
            file_name = file.name
            [unicode, _] = file_name.split('.')
            with open(os.path.join(image_folder, file_name), 'rb') as f:
                img_bytes = f.read()
            result = ocr.classification(img_bytes)
            if len(result) > 1:
                # 从字节数据创建图片
                img = Image.open(io.BytesIO(img_bytes))
                plt.imshow(img)
                plt.axis('off')  # 隐藏坐标轴
                plt.show()
                result = input('请输入正确的结果>>>')
                data[unicode] = result
            else:
                data[unicode] = result
        mf.write(json.dumps(data))
    上面这个方案可能还需要手动处理一下,因为模型识别不是百分百的准确率,我们暂且认为识别出一个字的就是正确的,两个字的就需要手动处理一下。


    12.png (110.47 KB, 下载次数: 0)
    下载附件
    2024-11-30 12:36 上传

    最后我们模拟请求一下,并从json文件中取对应的映射来替换不可见字符。
    import json
    import requests
    import re
    with open('mapping.json', 'r') as f:
        mapping = json.load(f)
    def get_data():
        headers = {
            "accept": "application/json, text/plain, */*",
            "accept-language": "zh-CN,zh;q=0.9,en;q=0.8,pt;q=0.7",
            "cache-control": "no-cache",
            "ismobile": "0",
            "pragma": "no-cache",
            "priority": "u=1, i",
            "referer": "脱敏信息",
            "sec-ch-ua": "\"Google Chrome\";v=\"131\", \"Chromium\";v=\"131\", \"Not_A Brand\";v=\"24\"",
            "sec-ch-ua-mobile": "?0",
            "sec-ch-ua-platform": "\"Windows\"",
            "sec-fetch-dest": "empty",
            "sec-fetch-mode": "cors",
            "sec-fetch-site": "same-origin",
            "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"
        }
        cookies = {}
        url = "脱敏信息"
        params = {}
        response = requests.get(url, headers=headers, cookies=cookies, params=params)
        print(response.text)
        content: str = response.json()['data']['chapterData']['content']
        content_list = re.findall(r'(.*?)
    ', content)
        for c in content_list:
            target_content = []
            for char in c:
                if '\\u' in repr(char):
                    hex_str = repr(char).replace('\\u', '')
                    target_char = mapping[str(int(hex_str.strip('\''), 16))]
                    target_content.append(target_char)
                else:
                    target_content.append(char.strip('\''))
            print(''.join(target_content))
    get_data()


    13.png (92.45 KB, 下载次数: 0)
    下载附件
    2024-11-30 12:37 上传

    成功!!!

    解析字体的第二种方案,加上一个好的文字识别模型,其实能够做到不用手动干预了,奈何本菜鸡不会训练模型。

    字体, 下载次数

  • FCGkitty   

    牛逼,学习新知识了。。。现在为什么都要对请求地址加密,不至于吧,这都是用啥加密的。。。
    YIUA   

    老哥你是真的高产
    littlewhite11
    OP
      


    FCGkitty 发表于 2024-11-30 13:05
    牛逼,学习新知识了。。。现在为什么都要对请求地址加密,不至于吧,这都是用啥加密的。。。

    就一个base64编码
    littlewhite11
    OP
      


    YIUA 发表于 2024-11-30 13:07
    老哥你是真的高产

    没有没有,巩固知识
    edgrdg   

    用心讨论,共获提升!
    Knm   

    感谢大佬分享
    CLYLR   

    感谢大佬分享 很厉害!
    Reze   

    感谢大佬分享的教程
    zfb38   

    解决问题的技术
    您需要登录后才可以回帖 登录 | 立即注册

    返回顶部