python打包的二进制文件反编译

简介

Pyhton是一个脚本语言,在运行Python代码时,最终由Python解释器来执行。解释器就是Python的运行环境,但是除了开发人员,大部分人在使用时并不会安装Python。所以官方就提供了一些打包程序,将代码与解释环境打包到二进制文件中,方便在各种操作系统中运行。

下面为一些文件格式:

  • .py: Python代码文件。对于一些开源项目,发布方也许会直接提供源码,但是使用时需要安装依赖库
  • .pyc: 源码编译后的中间式文件,其目的是为了加快下次运行时的速度。不能直接运行,需要python虚拟机的支持才可以运行,类似于java、.net平台的中的虚拟机,因此可以pyc文件可以跨平台执行。不同的python编译出的pyc文件是不同的,例如python2.4编译出的pyc文件,2.5不可以使用
  • 可执行文件:直接为专业人士提供可执行文件。你只需要知道如何使用它们。缺点是可移植性差,需要为不同的操作系统生成可执行文件。
    [En]

    executable files: provide executable files directly for professionals. You only need to know how to use them. The disadvantage is poor portability and need to generate executable files for different operating systems.*

下面是一些打包为可执行文件的方法:

python打包的二进制文件反编译

下面的实验,我主要使用pyinstaller打包文件。

Python文件打包

配置说明

下载方法:

pip install pyinstaller

或者使用打包好的包,访问:http://www.pyinstaller.org/downloads.html 下载对应平台的压缩文件,解压后打开解压路径,执行

python setup.py install

验证

pyinstaller -v

python打包的二进制文件反编译

参数

-F 制作独立的可执行程序
-D 制作出的档案存放在同一个文件夹下(默认值)
-K 包含TCL/TK(对于使用了TK的,最好加上这个选项,否则在未安装TK的电脑上无法运行)
-w 制作窗口程序
-c 制作命令行程序(默认)
-X 制作使用UPX压缩过的可执行程序(推荐使用这个选项,需要下载UPX包,解压后upx.exe放在Python(非PyInstaller)安装目录下,下载upx308w.zip)
-o DIR 指定输出SPEC文件路径(这也决定了最后输出的exe文件路径)
–icon=[ICO文件路径] 指定程序图标
-v [指定文件] 指定程序版本信息
-n [指定程序名] 指定程序名称

Windows:生成exe文件

正常使用

pyinstaller -F test.py

可执行文件存储在dist/test文件夹中,不过我这环境出了点问题,使用

pyinstaller -D test.py

python打包的二进制文件反编译

进入build/test文件夹,将Python环境安装处的Python37.dll拷贝到目录,再执行该目录下的test.exe

python打包的二进制文件反编译

Linux:生成ELF

下载方法和windows平台相同。

python打包的二进制文件反编译

打包生成ELF

pyinstaller -F test.py

python打包的二进制文件反编译

pyinstaller打包程序识别

exe图标特征

python打包的二进制文件反编译

exe字符串特征

python打包的二进制文件反编译

和Python语句很类似__file__,__main___,此外也有很多Python的特征

python打包的二进制文件反编译

ELF字符串特征

python打包的二进制文件反编译

ELF主函数特征

在Linux上运行pyintaller打包的可执行文件时,它会将打包好的文件解压到临时文件夹(/tmp)中的_MEIxxxxxx 路径中暂时存放,执行完毕之后再删除。所以在主函数起始位置会有如下特征

python打包的二进制文件反编译

反编译文件

我们要将Python打包成的可执行文件,首先要利用archive_viewer.py将exe/elf反编译为pyc字节码,再使用uncompyle6反编译为py文件。

反编译为pyc文件

archive_viewer.py就在我们的pyinstaller安装包文件夹中。例如我的Python环境中:

Win:D:\Anaconda\Lib\site-packages\PyInstaller\utils\cliutils

Linux:/home/ubuntu/.local/lib/python3.6/site-packages/PyInstaller/utils/cliutils

参数

U: go Up one level
O
X
Q: quit

首先,我们要解析EXE/ELF文件数据包,将可执行文件放置到与archive_viewer.py同目录下,使用命令:

python archive_viewer.py test.py

python打包的二进制文件反编译

有很重要的两点:

  1. 反编译EXE/ELF文件的Python版本必须与打包时的版本一致
  2. 反编译的pyc文件命名必须遵照上面红框中的”test”

其次,提取test.pyc和struct.pyc文件(后面会说明该文件作用)

? x test
to filename? test.pyc
? x struct
to filename? struct.pyc

python打包的二进制文件反编译

Linux端相同

python打包的二进制文件反编译

下面Win和Linux端操作相同,只讲述Win端。

文件修补

使用pyinstaller打包的文件,文件头的数据会被抹消掉。再还原的过程中,我们需要手动进行修补。文件头的格式为:magic(4字节,编译器标志) + 时间戳(4字节)。在实际修补时,需要添加的数据可能不止是8个字节。

将test.pyc和struct.pyc对比

python打包的二进制文件反编译

struct.pyc比test.pyc多出16字节,将这16字节插入test.pyc的头部

python打包的二进制文件反编译

反编译字节码文件

这里需要使用到uncompyle6(如果是Python2.7需要使用uncompyle2)

安装

pip install uncompyle6

或者

git clone https://github.com/rocky/python-uncompyle6.git
cd python-uncompyle6
python setup.py install

反编译

uncompyle6 test.pyc > test.py

python打包的二进制文件反编译

实战演练

准备

signin.exe:

python打包的二进制文件反编译

我们的目标是获取程序登录的账户密码。

python打包的二进制文件反编译

未加壳的64位可执行文件。

程序分析

python打包的二进制文件反编译

可以判断出,这个程序实际上是由Python打包成的可执行文件,且在运行这个程序时,在同目录下产生了一个tmp.dll文件,猜测是程序调用某些函数的接口。

反编译

使用archive_viewer.py反编译为字节码文件

python archive_viewer.py signin.exe

python打包的二进制文件反编译

修补文件

python打包的二进制文件反编译

55 0D 0D 0A 00 00 00 00 70 79 69 30 10 01 00 00

python打包的二进制文件反编译

程序是在Python3.8环境下打包,因此我们需要在Python3.8下使用uncompyle6

uncompyle6 main.pyc > main.py

得到py文件

1 # uncompyle6 version 3.7.2
 2 # Python bytecode 3.8 (3413)
 3 # Decompiled from: Python 3.8.0 (tags/v3.8.0:fa919fd, Oct 14 2019, 19:37:50) [MSC v.1916 64 bit (AMD64)]
 4 # Embedded file name: main.py
 5 # Compiled at: 1995-09-28 00:18:56
 6 # Size of source mod 2**32: 272 bytes
 7 import sys
 8 from PyQt5.QtCore import *
 9 from PyQt5.QtWidgets import *
10 from signin import *
11 from mydata import strBase64
12 from ctypes import *
13 import _ctypes
14 from base64 import b64decode
15 import os
16
17 class AccountChecker:
18
19     def __init__(self):
20         self.dllname = './tmp.dll'
21         self.dll = self._AccountChecker__release_dll()
22         self.enc = self.dll.enc
23         self.enc.argtypes = (c_char_p, c_char_p, c_char_p, c_int)
24         self.enc.restype = c_int
25         self.accounts = {b'SCTFer': b64decode(b'PLHCu+fujfZmMOMLGHCyWWOq5H5HDN2R5nHnlV30Q0EA')}
26         self.try_times = 0
27
28     def __release_dll(self):
29         with open(self.dllname, 'wb') as (f):
30             f.write(b64decode(strBase64.encode('ascii')))
31         return WinDLL(self.dllname)
32
33     def clean(self):
34         _ctypes.FreeLibrary(self.dll._handle)
35         if os.path.exists(self.dllname):
36             os.remove(self.dllname)
37
38     def _error(self, error_code):
39         errormsg = {0:'Unknown Error',
40          1:'Memory Error'}
41         QMessageBox.information(None, 'Error', errormsg[error_code], QMessageBox.Abort, QMessageBox.Abort)
42         sys.exit(1)
43
44     def __safe(self, username: bytes, password: bytes):
45         pwd_safe = b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
46         status = self.enc(username, password, pwd_safe, len(pwd_safe))
47         return (pwd_safe, status)
48
49     def check(self, username, password):
50         self.try_times += 1
51         if username not in self.accounts:
52             return False
53         encrypted_pwd, status = self._AccountChecker__safe(username, password)
54         if status == 1:
55             self._AccountChecker__error(1)
56         if encrypted_pwd != self.accounts[username]:
57             return False
58         self.try_times -= 1
59         return True
60
61
62 class SignInWnd(QMainWindow, Ui_QWidget):
63
64     def __init__(self, checker, parent=None):
65         super().__init__(parent)
66         self.checker = checker
67         self.setupUi(self)
68         self.PB_signin.clicked.connect(self.on_confirm_button_clicked)
69
70     @pyqtSlot()
71     def on_confirm_button_clicked(self):
72         username = bytes((self.LE_usrname.text()), encoding='ascii')
73         password = bytes((self.LE_pwd.text()), encoding='ascii')
74         if username == b'' or password == b'':
75             self.check_input_msgbox()
76         else:
77             self.msgbox(self.checker.check(username, password))
78
79     def check_input_msgbox(self):
80         QMessageBox.information(None, 'Error', 'Check Your Input!', QMessageBox.Ok, QMessageBox.Ok)
81
82     def msgbox(self, status):
83         msg_ex = {0:'',
84          1:'',
85          2:"It's no big deal, try again!",
86          3:'Useful information is in the binary, guess what?'}
87         msg = 'Succeeded! Flag is your password' if status else 'Failed to sign in\n' + msg_ex[(self.checker.try_times % 4)]
88         QMessageBox.information(None, 'SCTF2020', msg, QMessageBox.Ok, QMessageBox.Ok)
89
90
91 if __name__ == '__main__':
92     app = QApplication(sys.argv)
93     checker = AccountChecker()
94     sign_in_wnd = SignInWnd(checker)
95     sign_in_wnd.show()
96     app.exec()
97     checker.clean()
98     sys.exit()
99 # okay decompiling main.pyc

代码分析

通过代码我们能够了解到这些信息

1.

elf.dllname = './tmp.dll'

调用了tmp.dll文件作为接口。

2.

def __safe(self, username: bytes, password: bytes):
        pwd_safe = b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
        status = self.enc(username, password, pwd_safe, len(pwd_safe))
        return (pwd_safe, status)

    def check(self, username, password):
        self.try_times += 1
        if username not in self.accounts:
            return False
        encrypted_pwd, status = self._AccountChecker__safe(username, password)
        if status == 1:
            self._AccountChecker__error(1)
        if encrypted_pwd != self.accounts[username]:
            return False
        self.try_times -= 1
        return True
self.accounts = {b'SCTFer': b64decode(b'PLHCu+fujfZmMOMLGHCyWWOq5H5HDN2R5nHnlV30Q0EA')}

调用tmp.dll文件中的enc函数,传入username, password, pwd_safe, len(pwd_safe),实际就是将password加密后存储到pwd_safe字节码中。最后用pwd_safe与b64decode(b’PLHCu+fujfZmMOMLGHCyWWOq5H5HDN2R5nHnlV30Q0EA’)比较,且我们能够了解到用户名应该是SCTFer,且最后返回的status一个为非1。

打开tmp.dll文件,找到enc函数

python打包的二进制文件反编译

观察代码,实际操作可以分为两部分,逆向分析

异或操作

第47~54行代码实际上就是将Dst与用户名循环异或,最后得到b64decode(b’PLHCu+fujfZmMOMLGHCyWWOq5H5HDN2R5nHnlV30Q0EA’),因此我们只需要逆向异或就能得到加密后的Dst

from base64 import *

username = "SCTFer"
pwd_safe = b64decode('PLHCu+fujfZmMOMLGHCyWWOq5H5HDN2R5nHnlV30Q0EA')
# print (len(pwd_safe))
num = ["%02x" % x for x in pwd_safe]
hex_num = [int(x, 16) for x in num]

print(num)
# print (len(num))
for i in range(32):
    hex_num[i] ^= ord(username[i % len(username)])
# print (hex_num)
hex_nums = bytes.fromhex(''.join([hex(x)[2:].rjust(2, '0') for x in hex_num]))

print (hex_nums)

得到

b’o\xf2\x96\xfd\x82\x9c\xde\xb52v\x86yK3\xe6\x1f\x06\xd8\xb7=\x13J\xb8\xe3\xb52\xb3\xd38\x86\x10\x02\x00′

加密操作

每次传入了8字节数据进行加密(总共64字节),打开sub_180011311函数

python打包的二进制文件反编译

仔细观察代码,实际上这部分代码是使用CRC32的查表法,对数据进行加密。

加密原理实际上就是CRC32算法—输入一组长度48的字符串,每8个字节分为1组,共6组。对每一组取首位,判断正负。正值,左移一位;负值,左移一位,再异或0xB0004B7679FA26B3。重复判断操作64次,得到查表法所用的表。

因此我们只需要将整个加密过程逆向操作得到查表法的表,再进行CRC64计算,就能得到输入。

secret = []

# for i in range(4):
#     secret.append(int(hex_nums[i*8:(i + 1) * 8][::-1].hex(),16))

for i in range(4):
    secret.append(int.from_bytes(hex_nums[i*8:(i + 1) * 8], byteorder="little"))

print (secret)

key = 0xB0004B7679FA26B3

flag = ""

for s in secret:
    for i in range(64):
        sign = s & 1
        if sign == 1:
            s ^= key
        s //= 2
        if sign == 1:
            s |= 0x8000000000000000
    print(hex(s))
    j = 0
    while j < 8:
        flag += chr(s&0xFF)
        s >>= 8
        j += 1
print(flag)

因为计算机中采用小端排序,因此需要注意分组倒序。得到

python打包的二进制文件反编译

脚本

from base64 import *

username = "SCTFer"
pwd_safe = b64decode('PLHCu+fujfZmMOMLGHCyWWOq5H5HDN2R5nHnlV30Q0EA')
# print (len(pwd_safe))
num = ["%02x" % x for x in pwd_safe]
hex_num = [int(x, 16) for x in num]

print(num)
# print (len(num))
for i in range(32):
    hex_num[i] ^= ord(username[i % len(username)])
# print (hex_num)
hex_nums = bytes.fromhex(''.join([hex(x)[2:].rjust(2, '0') for x in hex_num]))

print (hex_nums)

secret = []

# for i in range(4):
#     secret.append(int(hex_nums[i*8:(i + 1) * 8][::-1].hex(),16))

for i in range(4):
    secret.append(int.from_bytes(hex_nums[i*8:(i + 1) * 8], byteorder="little"))

print (secret)

key = 0xB0004B7679FA26B3

flag = ""

for s in secret:
    for i in range(64):
        sign = s & 1
        if sign == 1:
            s ^= key
        s //= 2
        if sign == 1:
            s |= 0x8000000000000000
    print(hex(s))
    j = 0
    while j < 8:
        flag += chr(s&0xFF)
        s >>= 8
        j += 1
print(flag)

账户密码

username:SCTFer
password:SCTF{We1c0m3_To_Sctf_2020_re_!!}

python打包的二进制文件反编译

Original: https://www.cnblogs.com/Mayfly-nymph/p/13237019.html
Author: Hk_Mayfly
Title: python打包的二进制文件反编译

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/10648/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

免费咨询
免费咨询
扫码关注
扫码关注
联系站长

站长Johngo!

大数据和算法重度研究者!

持续产出大数据、算法、LeetCode干货,以及业界好资源!

2022012703491714

微信来撩,免费咨询:xiaozhu_tec

分享本页
返回顶部
最近整理资源【免费获取】:   👉 程序员最新必读书单  | 👏 互联网各方向面试题下载 | ✌️计算机核心资源汇总