python打包的二進制文件反編譯


簡介

Pyhton是一個腳本語言,在運行Python代碼時,最終由Python解釋器來執行。解釋器就是Python的運行環境,但是除了開發人員,大部分人在使用時並不會安裝Python。所以官方就提供了一些打包程序,將代碼與解釋環境打包到二進制文件中,方便在各種操作系統中運行。

下面為一些文件格式:

  • .py: Python代碼文件。對於一些開源項目,發布方也許會直接提供源碼,但是使用時需要安裝依賴庫
  • .pyc: 源碼編譯后的中間式文件,其目的是為了加快下次運行時的速度。不能直接運行,需要python虛擬機的支持才可以運行,類似於java、.net平台的中的虛擬機,因此可以pyc文件可以跨平台執行。不同的python編譯出的pyc文件是不同的,例如python2.4編譯出的pyc文件,2.5不可以使用
  • 可執行文件:針對得專業人員,直接提供可執行文件,只需要了解使用方法即可,缺點是可移植性差,需要針對不同的操作系統,生成可執行文件。

下面是一些打包為可執行文件的方法:


下面的實驗,我主要使用pyinstaller打包文件。 

 

Python文件打包

配置說明

下載方法:

pip install pyinstaller

或者使用打包好的包,訪問:http://www.pyinstaller.org/downloads.html 下載對應平台的壓縮文件,解壓后打開解壓路徑,執行

python setup.py install

 

驗證

pyinstaller -v

 

參數

-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

進入build/test文件夾,將Python環境安裝處的Python37.dll拷貝到目錄,再執行該目錄下的test.exe

 

Linux:生成ELF

下載方法和windows平台相同。

打包生成ELF

pyinstaller -F test.py

 

pyinstaller打包程序識別

exe圖標特征

 

exe字符串特征

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

 

ELF字符串特征

 

ELF主函數特征

在Linux上運行pyintaller打包的可執行文件時,它會將打包好的文件解壓到臨時文件夾(/tmp)中的_MEIxxxxxx 路徑中暫時存放,執行完畢之后再刪除。所以在主函數起始位置會有如下特征

 

反編譯文件

我們要將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 <name>: open embedded archive name

X <name>: extract name

Q: quit

 

首先,我們要解析EXE/ELF文件數據包,將可執行文件放置到與archive_viewer.py同目錄下,使用命令:

python archive_viewer.py test.py

有很重要的兩點:

  1. 反編譯EXE/ELF文件的Python版本必須與打包時的版本一致
  2. 反編譯的pyc文件命名必須遵照上面紅框中的“test”

 

其次,提取test.pyc和struct.pyc文件(后面會說明該文件作用)

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

Linux端相同

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

 

文件修補

使用pyinstaller打包的文件,文件頭的數據會被抹消掉。再還原的過程中,我們需要手動進行修補。文件頭的格式為:magic(4字節,編譯器標志) + 時間戳(4字節)。在實際修補時,需要添加的數據可能不止是8個字節。

將test.pyc和struct.pyc對比

struct.pyc比test.pyc多出16字節,將這16字節插入test.pyc的頭部

 

反編譯字節碼文件

這里需要使用到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

 

實戰演練

准備

signin.exe:

我們的目標是獲取程序登錄的賬戶密碼。

未加殼的64位可執行文件。

 

程序分析

可以判斷出,這個程序實際上是由Python打包成的可執行文件,且在運行這個程序時,在同目錄下產生了一個tmp.dll文件,猜測是程序調用某些函數的接口。

 

反編譯

使用archive_viewer.py反編譯為字節碼文件

python archive_viewer.py signin.exe

 

修補文件

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

 

程序是在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函數

觀察代碼,實際操作可以分為兩部分,逆向分析

 

異或操作

第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函數

仔細觀察代碼,實際上這部分代碼是使用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)

因為計算機中采用小端排序,因此需要注意分組倒序。得到

 

腳本

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_!!}

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM