记一次PyInstaller逆向

起因

看到坛友求助的一个python破解问题:

大佬们帮忙看下这个软件汇编的思路 – 吾爱破解 – 52pojie.cn

首先说明,我不熟悉pyInstaller,这是一次尝试,期间也查了很多资料和教程,如果有更好的方法还请诸位大佬赐教!

简单分析

查壳

使用Die查壳,结果如下:

image
​可以看到,程序是使用python编写、PyInstaller打包的

解包

既然是使用PyInstaller打包的,那么我们可以尝试解包它,看看能不能反编译出python代码

解包网站:PyInstaller Extractor WEB

上传AML.exe解包,网站会返回一个压缩包

image

解压压缩包,我们会得到一堆文件

image

找到AML.pyc,通过网站反编译为py代码

image

注意选择uncompyle6引擎,因为相较于pycdc,反编译的代码更完整。

下面是反编译AML.pyc的代码(代码较多,只放出关键部分):

import importlib, os, sys, threading, time 
from time import sleep
from tkinter import *
from tkinter import messagebox
from tkinter.ttk import *
from BootUSB import BootUSB
from pyamlboot.aml_verifysn_s905lsb_ import isVerify, reg #导入pyamlboot
from pyamlboot.rkamlboot import RKAmlogicSoC
from usb_manager import USBManager
from six.moves.http_cookies import SimpleCookie
num = 0
while isVerify() == False: #授权验证
if num > 0:
sys.exit()
else:
reg()
num += 1

if __name__ == "__main__":
win = WinGUI()
win.mainloop()

如果isVerify()返回False,说明验证失败,程序会尝试注册一次(num从0开始,第一次调用reg(),然后num变为1,下次循环时num>0就会退出)

可以发现授权验证部分在pyamlboot​包中,找到对应的pyamlboot文件夹

image

反编译aml_verifysn_s905lsb_.pyc​,关键代码如下(可以使用ai分析):

import time 
import tkinter
import winreg
from tkinter import END
import M2Crypto
from pyamlboot import sn
REG_KEY = '_aml_s905l3_sb' # 注册表键名
PEM = '2D2D2D2D2D424547494E205055424C4943204B45592D2D2D2D2D0A4D4947664D413047435371475349623344514542415155414134474E4144434269514B42675143564D6C396E635659395A616259496D3862583268756F504F620A4B617950334165704A47355774765A544F6C416F3859304737665A6541693941636A4745696A4A63547A465932773977347033454A5676767939464977744D4A0A3455353157613334647653524F683673356C59574B4F59716A2F74414868624A507650705873612F58416139467A376A7569735769772B52725458564A4A744C0A6C7652514F50444434616B44592B34796F514944415141420A2D2D2D2D2D454E44205055424C4943204B45592D2D2D2D2D' #十六进制编码的RSA公钥
key_path = 'SOFTWARE\\Software' # 注册表位置

def decrypt(msg):
key = bytes.fromhex(PEM) # 将十六进制公钥转字节
bio = M2Crypto.BIO.MemoryBuffer(key)
rsa_pub = M2Crypto.RSA.load_pub_key_bio(bio) # 加载公钥
return rsa_pub.public_decrypt(msg, M2Crypto.RSA.pkcs1_padding) # 公钥解密


def verify_reg(code):
try:
if not code: return False
# 解密注册码(HEX转字节->解密->解码)
plain = decrypt(bytes.fromhex(code)).decode()


# 格式校验(需包含冒号分隔符)
if ':' not in plain: return False
sn_part, date_part = plain.split(':',1)


# 硬件校验(比对磁盘序列号)
if sn.get_disk_info() != sn_part: return False


# 有效期校验(日期格式YYYYMMDD)
expire_date = int(date_part.replace('-',''))
current_date = int(time.strftime('%Y%m%d'))
return expire_date > current_date


except Exception:
return False

def reg():
import tkinter as tk
ScrolledText = ScrolledText
import tkinter.scrolledtext
root = tk.Tk()
curWidth = 400
curHight = 320
(scn_w, scn_h) = root.maxsize()
cen_x = (scn_w - curWidth) / 2
cen_y = (scn_h - curHight) / 2
size_xy = '%dx%d+%d+%d' % (curWidth, curHight, cen_x, cen_y)
root.geometry(size_xy)
root.resizable(0, 0)
root.title('软件注册')
label_frame = tk.LabelFrame(root, '机器码', **('text',))
label_frame.grid(0, 0, 10, 10, **('row', 'column', 'padx', 'pady'))
_sn = sn.get_disk_info()
isVerify = verify_reg(verify_register_info(REG_KEY))
machine_code = tk.StringVar(root, _sn, **('value',))
entryMachineCode = tk.Entry(label_frame, machine_code, 30, **('textvariable', 'width'))
if isVerify:
entryMachineCode.configure('disabled', **('state',))
entryMachineCode.grid(0, 0, **('row', 'column'))
result_frame = tk.LabelFrame(root, '注册码', **('text',))
result_frame.grid(1, 0, 10, 10, **('row', 'column', 'padx', 'pady'))
text_widget = ScrolledText(result_frame, 50, 10, **('width', 'height'))
if isVerify:
text_widget.insert(END, '软件已注册')
text_widget.configure('disable', **('state',))
text_widget.pack(True, 'both', **('expand', 'fill'))


def callbackMachineCode(event = None):
entryMachineCode.event_generate('<<Copy>>')

menuPackage = tk.Menu(root, False, **('tearoff',))
menuPackage.add_command('复制', callbackMachineCode, **('label', 'command'))


def popupMachineCode(event = None):
menuPackage.post(event.x_root, event.y_root)

entryMachineCode.bind('<Button-3>', popupMachineCode)


def register():
code_input = text_widget.get('1.0', 'end-1c')
isVerify = verify_reg(code_input)
if isVerify:
write_register_info(REG_KEY, code_input) # 写入注册表
tk.messagebox.showinfo('成功', '恭喜', **('message',))
root.destroy()
return True
None.messagebox.showinfo('警告!', '不是合法的注册码', **('message',))
return False

button = tk.Button(root, '注册', register, 'disabled' if isVerify else 'active', **('text', 'command', 'state'))
button.grid(2, 0, **('row', 'column'))
root.mainloop()


def isVerify():
if not verify_reg(verify_register_info(REG_KEY)):
del_winreg() # 验证失败时删除注册表项
return False
return True


def delete_key_value():
pass
# WARNING: Decompyle incomplete


def del_winreg():
delete_key_value()


def write_register_info(key, value):
pass
# WARNING: Decompyle incomplete


def verify_register_info(key):
registry_key = winreg.ConnectRegistry(None, winreg.HKEY_CURRENT_USER)
# WARNING: Decompyle incomplete

有一部分没反编译成功,但不影响分析

经分析,程序通过公钥解密注册码,将解密后的数据与校验码对比,如果一致就注册成功

程序的校验码应该为:”机器码:到期时间”

程序使用了RSA算法,没有私钥没法写注册机,所以只能另辟蹊径

修改内存

既然公钥是直接写在代码中的,那么我们可以将内置的公钥替换为我们自己的公钥

image

随便生成一对公私钥,注意密钥长度为1024(这里对比程序内置的公钥可以发现)

将公钥转为hex,记下来

image

打开Cheat Engine,选择AML.exe(一般是下面一个,是后出现的)

image

image

扫描的值就是程序内置的公钥:

2D2D2D2D2D424547494E205055424C4943204B45592D2D2D2D2D0A4D4947664D413047435371475349623344514542415155414134474E4144434269514B42675143564D6C396E635659395A616259496D3862583268756F504F620A4B617950334165704A47355774765A544F6C416F3859304737665A6541693941636A4745696A4A63547A465932773977347033454A5676767939464977744D4A0A3455353157613334647653524F683673356C59574B4F59716A2F74414868624A507650705873612F58416139467A376A7569735769772B52725458564A4A744C0A6C7652514F50444434616B44592B34796F514944415141420A2D2D2D2D2D454E44205055424C4943204B45592D2D2D2D2D

image
​点击确定后就修改成功了

好了,接下来我们就可以用私钥 加密 校验码 生成 注册码了

机器码就是程序上显示的:

image

校验码需要加上到期时间,随便构造一个:1136987320:20251213​

用rsa加密校验码:

image

注意选择私钥加密

输入到程序里,注册成功

image

上一篇
下一篇