起因

  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中的文件进行解密

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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
把代码稍微处理一下,删除答题等多余代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
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,经过测试 可以正常使用