← Back

关于某ewt刷课软件的逆向解包反编译

python逆向刷课

起因

  1. 该软件分为多个版本,在Github声称完全开源,实际上并非如此,开源的版本已经停止维护,而正在维护的版本却未开源,有人发Issue问其源码,而开发者却不予理会
  2. 维护中的软件编写实在是过于"奇特",ui为PyQt5编写,界面几乎全英文/历史某个版本非常好用且简洁,后来作者使用PyQt5重构时,体积膨胀到近50mb)
  3. 旧版本简洁软件甚至有作者写的版本检测导致无法继续使用

关于软件

Python3.10 + PyQt5 + Pyinstaller打包

解包&解密

既然已经知道他是用Pyinstaller打包成exe,那么可以使用对应工具对其解包 这里我采用pyinstxtractor对其解包 打开cmd 输入命令 python pyinstxtractor.py xxxx.exe 运行过程中发现部分文件解包报错 是加密了,这时我们来到解包后的目录中 image.png 在其中找到文件pyimod00_crypto_key 并且加上后缀.pyc image-7.png 这个是存储加解密的Key所在文件,对其进行反编译 这里我使用的是 在线Python pyc文件编译与反编译 进行在线反编译 image-4.png key已经出来了 接下来我们需要编写脚本对目录PYZ-00.pyz_extracted中的文件进行解密

import glob
import zlib
import tinyaes
from pathlib import Path

CRYPT_BLOCK_SIZE = 16

# key obtained from pyimod00_crypto_key
key = bytes('xqocyhnronyhveon', 'utf-8')

for p in Path("PYZ-00.pyz_extracted").glob("**/*.pyc.encrypted"):
    inf = open(p, 'rb') # encrypted file input
    outf = open(p.with_name(p.stem), 'wb') # output file

    # Initialization vector
    iv = inf.read(CRYPT_BLOCK_SIZE)

    cipher = tinyaes.AES(key, iv)

    # Decrypt and decompress
    plaintext = zlib.decompress(cipher.CTR_xcrypt_buffer(inf.read()))

    # Write pyc header
    # The header below is for Python 3.10 适用于3.10的头文件
    outf.write(b'\x6f\x0d\x0d\x0a\0\0\0\0\0\0\0\0\0\0\0\0') #需要替换成被反编译软件的对应python版本的头文件

    # Write decrypted data
    outf.write(plaintext)

    inf.close()
    outf.close()

    # Delete .pyc.encrypted file
    p.unlink()

注意,在outf.write中,我们需要替换为对应python版本的头文件 对于不同pyton版本头文件(header)也不相同,2.7~3.10如下所示:

Python 2.7: \x03\xf3\x0d\x0a\0\0\0\0 Python 3.0: \x3b\x0c\x0d\x0a\0\0\0\0 Python 3.1: \x4f\x0c\x0d\x0a\0\0\0\0 Python 3.2: \x6c\x0c\x0d\x0a\0\0\0\0 Python 3.3: \x9e\x0c\x0d\x0a\0\0\0\0\0\0\0\0 Python 3.4: \xee\x0c\x0d\x0a\0\0\0\0\0\0\0\0 Python 3.5: \x17\x0d\x0d\x0a\0\0\0\0\0\0\0\0 Python 3.6: \x33\x0d\x0d\x0a\0\0\0\0\0\0\0\0 Python 3.7: \x42\x0d\x0d\x0a\0\0\0\0\0\0\0\0\0\0\0\0 Python 3.8: \x55\x0d\x0d\x0a\0\0\0\0\0\0\0\0\0\0\0\0 Python 3.9: \x61\x0d\x0d\x0a\0\0\0\0\0\0\0\0\0\0\0\0 Python 3.10: \x6f\x0d\x0d\x0a\0\0\0\0\0\0\0\0\0\0\0\0

其他版本的也可以通过查看对应python版本的.py的头文件获取 在上一步的反编译中,会显示该pyc的python版本(# Version : Python 3.10)
选对应的头文件替换即可 然后我们运行脚本 现目录中加密后的文件都变成*.pyc了 这个时候我们反编译即可 image.png

反编译

寻找了一会,发现了部分关键pyc文件 并且将其反编译成了py文件image-6.png ui.py``start_ui.py 是该软件的UI界面代码 functions.py ewt_core.py 则为关于某网课的部分Api实现以及刷课的核心代码

附属软件的逆向

起因:

该刷课软件如果想使用高倍数刷课,需要使用他的一个获取license的程序获取license,该获取license的程序获取前需要答对20题,过于麻烦于是便将他逆向。

image-8-1024x576.png

逆向

同上方逆向刷课软件过程一样,将其解包+解密,具体过程省略 在extracted目录中发现了get_license文件,改名为pyc后缀,并且将其反编译 却发现无法正常反编译 内容为空 这是因为他缺少了头文件,我们需要在同目录下的struct文件中复制它的头文件过去 这里使用工具winhex打开struct文件和get_license.pyc文件image-9.pngimage-11.png 对比一下可以看出get_license.pyc文件缺少了Offset:00000000中的字节 制一下struct中的过去即可image-12.png 这时我们就可以正常反编译了image-13.png 把代码稍微处理一下,删除答题等多余代码

import hashlib
import json
import os
import random
import requests

def creat_sign(account, license_grade):
    
    try:
        res = requests.get('https://f.m.suning.com/api/ct.do')
        data = json.loads(res.text)
        print(data)
        current_time = data['currentTime']
    finally:
        pass
    
    try:
        res = requests.get('http://api.m.taobao.com/rest/api3.do?api=mtop.common.getTimestamp')
        data = json.loads(res.text)
        current_time = int(data['data']['t'])
        print(data)
    finally:
        pass
        
    expiration = current_time + 1200
    license_type = 1
    sign_text = f'''a={account}&expiration={expiration}&t={license_type}&g={license_grade}&salt=xxur0m9u0qvb598uyxi9rmur9qvmurc0qmrucm0rt9quynv'''
    s = hashlib.md5(sign_text.encode('utf8')).hexdigest().upper()
    out = [
        {
            'account': account,
            'expiration': expiration,
            'license_type': license_type,
            'license_grade': license_grade,
            'signature': s }]
    print('以下是你的license。使用方法:在程序目录创建license.txt,将以下文本复制进去,保存,然后启动GUI')
    print(json.dumps(out))
    return None                                            

print('v2 license,有效期20分钟')
account = input('输入你的网课账号: ')
creat_sign(account, 2)

os.system('pause')

ok,直接跑起来输入账号获取license,经过测试 可以正常使用