pyc 文件 ¶
code object¶
在我們導入 python 腳本時在目錄下會生成個一個相應的 pyc 文件,是 pythoncodeobj 的持久化儲存形式, 加速下一次的裝載。
文件結構 ¶
pyc 文件由三大部分組成
-
最開始 4 個字節是一個 Maigc int, 標識此 pyc 的版本信息
-
接下來四個字節還是個 int, 是 pyc 產生的時間
-
序列化的 PyCodeObject, 結構參照 include/code.h, 序列化方法 python/marshal
pyc 完整的文件解析可以參照
關於 co_code
一串二進制流, 代表着指令序列, 具體定義在 include/opcode.h 中, 也可以參照 python opcodes。
由
-
指令 (opcode), 分爲有參數和無參數兩種, 以 https://github.com/python/cpython/blob/fc7df0e664198cb05cafd972f190a18ca422989c/Include/opcode.h#L69 劃分
-
參數 (oparg)
python3.6 以上參數永遠佔 1 字節, 如果指令不帶參數的話則以0x00
代替, 在運行過程中被解釋器忽略, 也是 Stegosaurus 技術原理; 而低於 python3.5 的版本中指令不帶參數的話卻沒有0x00
填充
例題 ¶
首先嚐試 pycdc 反編譯失敗
# Source Generated with Decompyle++
# File: imgenc.pyc (Python 2.7)
import sys
import numpy as np
from scipy.misc import imread, imsave
def doit(input_file, output_file, f):
Unsupported opcode: STOP_CODE
img = imread(input_file, flatten = True)
img /= 255
size = img.shape[0]
# WARNING: Decompyle incomplete
注意到是 python2.7, 也就是說指令序列共佔 1 字節或 3 字節 (有參數無參數)
使用 pcads 得到
imgenc.pyc (Python 2.7)
...
67 STOP_CODE
68 STOP_CODE
69 BINARY_DIVIDE
70 JUMP_IF_TRUE_OR_POP 5
73 LOAD_CONST 3: 0
76 LOAD_CONST 3: 0
79 BINARY_DIVIDE
定位到出錯的地方, 觀察發現 LOAD_CONST LOAD_CONST BINARY_DIVIDE STORE_FAST opcodes (64 03 00 64 03 00 15 7d 05 00)
被破壞了, 根據上下文線索修復後
00000120 64 04 00 6b 00 00 72 ce 00 64 03 00 64 03 00 15 |d..k..r..d..d...|
00000130 7d 05 00 64 03 00 64 03 00 15 7d 05 00 64 03 00 |}..d..d...}..d..|
00000140 64 03 00 15 7d 05 00 64 03 00 64 03 00 15 7d 05 |d...}..d..d...}.|
00000150 00 64 03 00 64 03 00 15 7d 05 00 64 03 00 64 03 |.d..d...}..d..d.|
00000160 00 15 7d 05 00 64 03 00 64 03 00 15 7d 05 00 64 |..}..d..d...}..d|
00000170 03 00 64 03 00 15 7d 05 00 64 03 00 64 03 00 15 |..d...}..d..d...|
00000180 7d 05 00 64 03 00 64 03 00 15 7d 05 00 64 03 00 |}..d..d...}..d..|
00000190 64 03 00 15 7d 05 00 64 03 00 64 03 00 15 7d 05 |d...}..d..d...}.|
000001a0 00 64 03 00 64 03 00 15 7d 05 00 64 03 00 64 03 |.d..d...}..d..d.|
000001b0 00 15 7d 05 00 64 03 00 64 03 00 15 7d 05 00 6e |..}..d..d...}..n|
接下來根據修復好的 python 源代碼得到 flag 即可
延伸:
- 題目: 0ctf-2017:py
- writeup: 記一次手擼 CPython bytecode
Tools¶
pycdc¶
將 python 字節碼轉換爲可讀的 python 源代碼, 包含了反彙編 (pycads) 和反編譯 (pycdc) 兩種工具
Stegosaurus¶
Stegosaurus 是一款隱寫工具,它允許我們在 Python 字節碼文件 (pyc 或 pyo) 中嵌入任意 Payload。由於編碼密度較低,因此我們嵌入 Payload 的過程既不會改變源代碼的運行行爲,也不會改變源文件的文件大小。 Payload 代碼會被分散嵌入到字節碼之中,所以類似 strings 這樣的代碼工具無法查找到實際的 Payload。 Python 的 dis 模塊會返回源文件的字節碼,然後我們就可以使用 Stegosaurus 來嵌入 Payload 了。
原理是在 python 的字節碼文件中,利用冗餘空間,將完整的 payload 代碼分散隱藏到這些零零碎碎的空間中。
具體用法可參看 ctf-tools。
例題 ¶
Bugku QAQ
賽題鏈接如下:
http://ctf.bugku.com/files/447e4b626f2d2481809b8690613c1613/QAQ
http://ctf.bugku.com/files/5c02892cd05a9dcd1c5a34ef22dd9c5e/cipher.txt
首先拿到這道題,用 010Editor
乍一眼看過去,我們可以看到一些特徵信息:
可以判斷這是個跟 python
有關的東西,通過查閱相關資料可以判斷這是個 python
經編譯過後的 pyc
文件。這裏可能很多小夥伴們可能不理解了,什麼是 pyc
文件呢?爲什麼會生成 pyc
文件? pyc
文件又是何時生成的呢?下面我將一一解答這些問題。
簡單來說, pyc
文件就是 Python
的字節碼文件,是個二進制文件。我們都知道 Python
是一種全平臺的解釋性語言,全平臺其實就是 Python
文件在經過解釋器解釋之後 (或者稱爲編譯) 生成的 pyc
文件可以在多個平臺下運行,這樣同樣也可以隱藏源代碼。其實, Python
是完全面向對象的語言, Python
文件在經過解釋器解釋後生成字節碼對象 PyCodeObject
, pyc
文件可以理解爲是 PyCodeObject
對象的持久化保存方式。而 pyc
文件只有在文件被當成模塊導入時纔會生成。也就是說, Python
解釋器認爲,只有 import
進行的模塊才需要被重用。 生成 pyc
文件的好處顯而易見,當我們多次運行程序時,不需要重新對該模塊進行重新的解釋。主文件一般只需要加載一次,不會被其他模塊導入,所以一般主文件不會生成 pyc
文件。
我們舉個例子來說明這個問題:
爲了方便起見,我們事先創建一個 test 文件夾作爲此次實驗的測試:
mkdir test && cd test/
假設我們現在有個 test.py
文件,文件內容如下:
def print_test():
print('Hello,Kitty!')
print_test()
我們執行以下命令:
python3 test.py
不用說,想必大家都知道打印出的結果是下面這個:
Hello,Kitty!
我們通過下面命令查看下當前文件夾下有哪些文件:
ls -alh
我們可以發現,並沒有 pyc
文件生成。
‘我們再去創建一個文件爲 import_test.py
文件,文件內容如下:
注:
test.py
和import_test.py
應當放在同一文件夾下
import test
test.print_test()
我們執行以下命令:
python3 import_test.py
結果如下:
Hello,Kitty!
Hello,Kitty!
誒,爲啥會打印出兩句相同的話呢?我們再往下看,我們通過下面命令查看下當前文件夾下有哪些文件:
ls -alh
結果如下:
總用量 20K
drwxr-xr-x 3 python python 4.0K 11月 5 20:38 .
drwxrwxr-x 4 python python 4.0K 11月 5 20:25 ..
-rw-r--r-- 1 python python 31 11月 5 20:38 import_test.py
drwxr-xr-x 2 python python 4.0K 11月 5 20:38 __pycache__
-rw-r--r-- 1 python python 58 11月 5 20:28 test.py
誒,多了個 __pycache__
文件夾,我們進入文件夾下看看有什麼?
cd __pycache__ && ls
我們可以看到生成了一個 test.cpython-36.pyc
。爲什麼是這樣子呢?
我們可以看到,我們在執行 python3 import_test.py
命令的時候,首先開始執行的是 import test
,即導入 test
模塊,而一個模塊被導入時, PVM(Python Virtual Machine)
會在後臺從一系列路徑中搜索該模塊,其搜索過程如下:
- 在當前目錄下搜索該模塊
- 在環境變量
PYTHONPATH
中指定的路徑列表中依次搜索 - 在
python
安裝路徑中搜索
事實上, PVM
通過變量 sys.path
中包含的路徑來搜索,這個變量裏麪包含的路徑列表就是上面提到的這些路徑信息。
模塊的搜索路徑都放在了 sys.path
列表中,如果缺省的 sys.path
中沒有含有自己的模塊或包的路徑,可以動態的加入 (sys.path.apend)
即可。
事實上, Python
中所有加載到內存的模塊都放在 sys.modules
。當 import
一個模塊時首先會在這個列表中查找是否已經加載了此模塊,如果加載了則只是將模塊的名字加入到正在調用 import
的模塊的 Local
名字空間中。如果沒有加載則從 sys.path
目錄中按照模塊名稱查找模塊文件,模塊文件可以是 py
、 pyc
、 pyd
,找到後將模塊載入內存,並加入到 sys.modules
中,並將名稱導入到當前的 Local
名字空間。
可以看出來,一個模塊不會重複載入。多個不同的模塊都可以用 import
引入同一個模塊到自己的 Local
名字空間,其實背後的 PyModuleObject
對象只有一個。
在這裏,我還要說明一個問題,import
只能導入模塊,不能導入模塊中的對象 (類、函數、變量等)。例如像上面這個例子,我在 test.py
裏面定義了一個函數 print_test()
,我在另外一個模塊文件 import_test.py
不能直接通過 import test.print_test
將 print_test
導入到本模塊文件中,只能用 import test
進行導入。如果我想只導入特定的類、函數、變量,用 from test import print_test
即可。
既然說到了 import
導入機制,再提一提嵌套導入和 Package
導入。
import
嵌套導入
嵌套,不難理解,就是一個套着一個。小時候我們都玩過俄羅斯套娃吧,俄羅斯套娃就是一個大娃娃裏面套着一個小娃娃,小娃娃裏面還有更小的娃娃,而這個嵌套導入也是同一個意思。假如我們現在有一個模塊,我們想要導入模塊 A
,而模塊 A
中有含有其他模塊需要導入,比如模塊 B
,模塊 B
中又含有模塊 C
,一直這樣延續下去,這種方式我們稱之爲 import
嵌套導入。
對這種嵌套比較容易理解,我們需要注意的一點就是各個模塊的 Local
名字空間是獨立的,所以上面的例子,本模塊 import A
完了後,本模塊只能訪問模塊 A
,不能訪問 B
及其它模塊。雖然模塊 B
已經加載到內存了,如果要訪問,還必須明確在本模塊中導入 import B
。
那如果我們有以下嵌套這種情況,我們該怎麼處理呢?
比如我們現在有個模塊 A
:
# A.py
from B import D
class C:
pass
還有個模塊 B
:
# B.py
from A import C
class D:
pass
我們簡單分析一下程序,如果程序運行,應該會去從模塊 B 中調用對象 D。
我們嘗試執行一下 python A.py
:
報 ImportError
的錯誤,似乎是沒有加載到對象 D
,而我們將 from B import D
改成 import B
,我們似乎就能執行成功了。
這是怎麼回事呢?這其實是跟 Python
內部 import
的機制是有關的,具體到 from B import D
, Python
內部會分成以下幾個步驟:
- 在
sys.modules
中查找符號B
- 如果符號
B
存在,則獲得符號B
對應的module
對象<module B>
。從<module B>
的__dict__
中獲得符號D
對應的對象,如果D
不存在,則拋出異常 - 如果符號
B
不存在,則創建一個新的module
對象<module B>
,注意,此時module
對象的__dict__
爲空。執行B.py
中的表達式,填充<module B>
的__dict__
。從<module B>
的__dict__
中獲得D
對應的對象。如果D
不存在,則拋出異常。
所以,這個例子的執行順序如下:
1、執行 A.py
中的 from B import D
注:由於是執行的
python A.py
,所以在sys.modules
中並沒有<module B>
存在,首先爲B.py
創建一個module
對象 (<module B>
),注意,這時創建的這個module
對象是空的,裏邊啥也沒有,在Python
內部創建了這個module
對象之後,就會解析執行B.py
,其目的是填充<module B>
這個dict
。
2、執行 B.py
中的 from A import C
注:在執行
B.py
的過程中,會碰到這一句,首先檢查sys.modules
這個module
緩存中是否已經存在<module A>
了,由於這時緩存還沒有緩存<module A>
,所以類似的,Python
內部會爲A.py
創建一個module
對象 (<module A>
),然後,同樣地,執行A.py
中的語句。
3、再次執行 A.py
中的 from B import D
注:這時,由於在第
1
步時,創建的<module B>
對象已經緩存在了sys.modules
中,所以直接就得到了<module B>
,但是,注意,從整個過程來看,我們知道,這時<module B>
還是一個空的對象,裏面啥也沒有,所以從這個module
中獲得符號D
的操作就會拋出異常。如果這裏只是import B
,由於B
這個符號在sys.modules
中已經存在,所以是不會拋出異常的。
我們可以從下圖很清楚的看到 import
嵌套導入的過程:
Package
導入
包 (Package)
可以看成模塊的集合,只要一個文件夾下面有個 __init__.py
文件,那麼這個文件夾就可以看做是一個包。包下面的文件夾還可以成爲包 (子包)。更進一步的講,多個較小的包可以聚合成一個較大的包。通過包這種結構,我們可以很方便的進行類的管理和維護,也方便了用戶的使用。比如 SQLAlchemy
等都是以包的形式發佈給用戶的。
包和模塊其實是很類似的東西,如果查看包的類型: import SQLAlchemy type(SQLAlchemy)
,可以看到其實也是 <type 'module'>
。 import
包的時候查找的路徑也是 sys.path
。
包導入的過程和模塊的基本一致,只是導入包的時候會執行此包目錄下的 __init__.py
,而不是模塊裏面的語句了。另外,如果只是單純的導入包,而包的 __init__.py
中又沒有明確的其他初始化操作,那麼此包下面的模塊是不會自動導入的。
假設我們有如下文件結構:
.
└── PA
├── __init__.py
├── PB1
│ ├── __init__.py
│ └── pb1_m.py
├── PB2
│ ├── __init__.py
│ └── pb2_m.py
└── wave.py
wave.py
, pb1_m.py
, pb2_m.py
文件中我們均定義瞭如下函數:
def getName():
pass
__init__.py
文件內容均爲空。
我們新建一個 test.py
,內容如下:
import sys
import PA.wave #1
import PA.PB1 #2
import PA.PB1.pb1_m as m1 #3
import PA.PB2.pb2_m #4
PA.wave.getName() #5
m1.getName() #6
PA.PB2.pb2_m.getName() #7
我們運行以後,可以看出是成功執行成功了,我們再看看目錄結構:
.
├── PA
│ ├── __init__.py
│ ├── __init__.pyc
│ ├── PB1
│ │ ├── __init__.py
│ │ ├── __init__.pyc
│ │ ├── pb1_m.py
│ │ └── pb1_m.pyc
│ ├── PB2
│ │ ├── __init__.py
│ │ ├── __init__.pyc
│ │ ├── pb2_m.py
│ │ └── pb2_m.pyc
│ ├── wave.py
│ └── wave.pyc
└── test.py
我們來分析一下這個過程:
- 當執行
#1
後,sys.modules
會同時存在PA
、PA.wave
兩個模塊,此時可以調用PA.wave
的任何類或函數了。但不能調用PA.PB1(2)
下的任何模塊。當前Local
中有了PA
名字。 - 當執行
#2
後,只是將PA.PB1
載入內存,sys.modules
中會有PA
、PA.wave
、PA.PB1
三個模塊,但是PA.PB1
下的任何模塊都沒有自動載入內存,此時如果直接執行PA.PB1.pb1_m.getName()
則會出錯,因爲PA.PB1
中並沒有pb1_m
。當前Local
中還是隻有PA
名字,並沒有PA.PB1
名字。 - 當執行
#3
後,會將PA.PB1
下的pb1_m
載入內存,sys.modules
中會有PA
、PA.wave
、PA.PB1
、PA.PB1.pb1_m
四個模塊,此時可以執行PA.PB1.pb1_m.getName()
了。由於使用了as
,當前Local
中除了PA
名字,另外添加了m1
作爲PA.PB1.pb1_m
的別名。 - 當執行
#4
後,會將PA.PB2
、PA.PB2.pb2_m
載入內存,sys.modules
中會有PA
、PA.wave
、PA.PB1
、PA.PB1.pb1_m
、PA.PB2
、PA.PB2.pb2_m
六個模塊。當前Local
中還是隻有PA
、m1
。 - 下面的
#5
,#6
,#7
都是可以正確運行的。
注:需要注意的問題是如果
PA.PB2.pb2_m
想導入PA.PB1.pb1_m
、PA.wave
是可以直接成功的。最好是採用明確的導入路徑,對於../..
相對導入路徑還是不推薦使用。
既然我們已經知道 pyc
文件的產生,再回到那道賽題,我們嘗試將 pyc
文件反編譯回 python
源碼。我們使用在線的開源工具進行嘗試:
部分代碼沒有反編譯成功???我們可以嘗試分析一下,大概意思就是讀取 cipher.txt
那個文件,將那個文件內容是通過 base64
編碼的,我們的目的是將文件內容解碼,然後又已知 key
,通過 encryt
函數進行加密的,我們可以嘗試將代碼補全:
def encryt(key, plain):
cipher = ''
for i in range(len(plain)):
cipher += chr(ord(key[i % len(key)]) ^ ord(plain[i]))
return cipher
def getPlainText():
plain = ''
with open('cipher.txt') as (f):
while True:
line = f.readline()
if line:
plain += line
else:
break
return plain.decode('base_64')
def main():
key = 'LordCasser'
plain = getPlainText()
cipher = encryt(key, plain)
with open('xxx.txt', 'w') as (f):
f.write(cipher)
if __name__ == '__main__':
main()
結果如下:
YOU ARE FOOLED
THIS IS NOT THAT YOU WANT
GO ON DUDE
CATCH THAT STEGOSAURUS
提示告訴我們用 STEGOSAURUS
工具進行隱寫的,我們直接將隱藏的 payload 分離出來即可。
python3 stegosaurus.py -x QAQ.pyc
我們得到了最終的 flag
爲:flag{fin4lly_z3r0_d34d}
既然都說到這個份子上了,我們就來分析一下我們是如何通過 Stegosaurus
來嵌入 Payload
。
我們仍然以上面這個代碼爲例子,我們設置腳本名稱爲 encode.py
。
第一步,我們使用 Stegosaurus
來查看在不改變源文件 (Carrier)
大小的情況下,我們的 Payload
能攜帶多少字節的數據:
python3 -m stegosaurus encode.py -r
現在,我們可以安全地嵌入最多 24 個字節的 Payload
了。如果不想覆蓋源文件的話,我們可以使用 -s
參數來單獨生成一個嵌入了 Payload
的 py
文件:
python3 -m stegosaurus encode.py -s --payload "flag{fin4lly_z3r0_d34d}"
現在我們可以用 ls
命令查看磁盤目錄,嵌入了 Payload
的文件 ( carrier
文件) 和原始的字節碼文件兩者大小是完全相同的:
注:如果沒有使用
-s
參數,那麼原始的字節碼文件將會被覆蓋。
我們可以通過向 Stegosaurus
傳遞 -x
參數來提取出 Payload
:
python3 -m stegosaurus __pycache__/encode.cpython-36-stegosaurus.pyc -x
我們構造的 Payload
不一定要是一個 ASCII
字符串, shellcode
也是可以的:
我們重新編寫一個 example.py
模塊,代碼如下:
import sys
import os
import math
def add(a,b):
return int(a)+int(b)
def sum1(result):
return int(result)*3
def sum2(result):
return int(result)/3
def sum3(result):
return int(result)-3
def main():
a = 1
b = 2
result = add(a,b)
print(sum1(result))
print(sum2(result))
print(sum3(result))
if __name__ == "__main__":
main()
我們讓它攜帶 Payload
爲 flag_is_here
。
我們可以查看嵌入 Payload
之前和之後的 Python
代碼運行情況:
通過 strings
查看 Stegosaurus
嵌入了 Payload
之後的文件輸出情況 ( payload
並沒有顯示出來):
接下來使用 Python
的 dis
模塊來查看 Stegosaurus
嵌入 Payload
之前和之後的文件字節碼變化情況:
嵌入 payload 之前:
#( 11/29/18@ 5:14下午 )( python@Sakura ):~/桌面
python3 -m dis example.py
1 0 LOAD_CONST 0 (0)
2 LOAD_CONST 1 (None)
4 IMPORT_NAME 0 (sys)
6 STORE_NAME 0 (sys)
2 8 LOAD_CONST 0 (0)
10 LOAD_CONST 1 (None)
12 IMPORT_NAME 1 (os)
14 STORE_NAME 1 (os)
3 16 LOAD_CONST 0 (0)
18 LOAD_CONST 1 (None)
20 IMPORT_NAME 2 (math)
22 STORE_NAME 2 (math)
4 24 LOAD_CONST 2 (<code object add at 0x7f90479778a0, file "example.py", line 4>)
26 LOAD_CONST 3 ('add')
28 MAKE_FUNCTION 0
30 STORE_NAME 3 (add)
6 32 LOAD_CONST 4 (<code object sum1 at 0x7f9047977810, file "example.py", line 6>)
34 LOAD_CONST 5 ('sum1')
36 MAKE_FUNCTION 0
38 STORE_NAME 4 (sum1)
9 40 LOAD_CONST 6 (<code object sum2 at 0x7f9047977ae0, file "example.py", line 9>)
42 LOAD_CONST 7 ('sum2')
44 MAKE_FUNCTION 0
46 STORE_NAME 5 (sum2)
12 48 LOAD_CONST 8 (<code object sum3 at 0x7f9047977f60, file "example.py", line 12>)
50 LOAD_CONST 9 ('sum3')
52 MAKE_FUNCTION 0
54 STORE_NAME 6 (sum3)
15 56 LOAD_CONST 10 (<code object main at 0x7f904798c300, file "example.py", line 15>)
58 LOAD_CONST 11 ('main')
60 MAKE_FUNCTION 0
62 STORE_NAME 7 (main)
23 64 LOAD_NAME 8 (__name__)
66 LOAD_CONST 12 ('__main__')
68 COMPARE_OP 2 (==)
70 POP_JUMP_IF_FALSE 78
24 72 LOAD_NAME 7 (main)
74 CALL_FUNCTION 0
76 POP_TOP
>> 78 LOAD_CONST 1 (None)
80 RETURN_VALUE
嵌入 payload
之後:
#( 11/29/18@ 5:31下午 )( python@Sakura ):~/桌面
python3 -m dis example.py
1 0 LOAD_CONST 0 (0)
2 LOAD_CONST 1 (None)
4 IMPORT_NAME 0 (sys)
6 STORE_NAME 0 (sys)
2 8 LOAD_CONST 0 (0)
10 LOAD_CONST 1 (None)
12 IMPORT_NAME 1 (os)
14 STORE_NAME 1 (os)
3 16 LOAD_CONST 0 (0)
18 LOAD_CONST 1 (None)
20 IMPORT_NAME 2 (math)
22 STORE_NAME 2 (math)
4 24 LOAD_CONST 2 (<code object add at 0x7f146e7038a0, file "example.py", line 4>)
26 LOAD_CONST 3 ('add')
28 MAKE_FUNCTION 0
30 STORE_NAME 3 (add)
6 32 LOAD_CONST 4 (<code object sum1 at 0x7f146e703810, file "example.py", line 6>)
34 LOAD_CONST 5 ('sum1')
36 MAKE_FUNCTION 0
38 STORE_NAME 4 (sum1)
9 40 LOAD_CONST 6 (<code object sum2 at 0x7f146e703ae0, file "example.py", line 9>)
42 LOAD_CONST 7 ('sum2')
44 MAKE_FUNCTION 0
46 STORE_NAME 5 (sum2)
12 48 LOAD_CONST 8 (<code object sum3 at 0x7f146e703f60, file "example.py", line 12>)
50 LOAD_CONST 9 ('sum3')
52 MAKE_FUNCTION 0
54 STORE_NAME 6 (sum3)
15 56 LOAD_CONST 10 (<code object main at 0x7f146e718300, file "example.py", line 15>)
58 LOAD_CONST 11 ('main')
60 MAKE_FUNCTION 0
62 STORE_NAME 7 (main)
23 64 LOAD_NAME 8 (__name__)
66 LOAD_CONST 12 ('__main__')
68 COMPARE_OP 2 (==)
70 POP_JUMP_IF_FALSE 78
24 72 LOAD_NAME 7 (main)
74 CALL_FUNCTION 0
76 POP_TOP
>> 78 LOAD_CONST 1 (None)
80 RETURN_VALUE
注:
Payload
的發送和接受方法完全取決於用戶個人喜好,Stegosaurus
只提供了一種向Python
字節碼文件嵌入或提取Payload
的方法。但是爲了保證嵌入之後的代碼文件大小不會發生變化,因此Stegosaurus
所支持嵌入的Payload
字節長度十分有限。因此 ,如果你需要嵌入一個很大的Payload
,那麼你可能要將其分散存儲於多個字節碼文件中了。
爲了在不改變源文件大小的情況下向其嵌入 Payload
,我們需要識別出字節碼中的無效空間 ( Dead Zone
)。這裏所謂的無效空間指的是那些即使被修改也不會改變原 Python
腳本正常行爲的那些字節數據。
需要注意的是,我們可以輕而易舉地找出 Python3.6
代碼中的無效空間。 Python
的引用解釋器 CPython
有兩種類型的操作碼:即無參數的和有參數的。在版本號低於 3.5
的 Python
版本中,根據操作碼是否帶參,字節碼中的操作指令將需要佔用 1
個字節或 3
個字節。在 Python3.6
中就不一樣了, Python3.6
中所有的指令都佔用 2
個字節,並會將無參數指令的第二個字節設置爲 0
,這個字節在其運行過程中將會被解釋器忽略。這也就意味着,對於字節碼中每一個不帶參數的操作指令, Stegosaurus
都可以安全地嵌入長度爲 1
個字節的 Payload
代碼。
我們可以通過 Stegosaurus
的 -vv
選項來查看 Payload
是如何嵌入到這些無效空間之中的:
#( 11/29/18@10:35下午 )( python@Sakura ):~/桌面
python3 -m stegosaurus example.py -s -p "ABCDE" -vv
2018-11-29 22:36:26,795 - stegosaurus - DEBUG - Validated args
2018-11-29 22:36:26,797 - stegosaurus - INFO - Compiled example.py as __pycache__/example.cpython-36.pyc for use as carrier
2018-11-29 22:36:26,797 - stegosaurus - DEBUG - Read header and bytecode from carrier
2018-11-29 22:36:26,798 - stegosaurus - DEBUG - POP_TOP (0)
2018-11-29 22:36:26,798 - stegosaurus - DEBUG - POP_TOP (0)
2018-11-29 22:36:26,798 - stegosaurus - DEBUG - POP_TOP (0)
2018-11-29 22:36:26,798 - stegosaurus - DEBUG - RETURN_VALUE (0)
2018-11-29 22:36:26,798 - stegosaurus - DEBUG - BINARY_SUBTRACT (0)
2018-11-29 22:36:26,798 - stegosaurus - DEBUG - RETURN_VALUE (0)
2018-11-29 22:36:26,798 - stegosaurus - DEBUG - BINARY_TRUE_DIVIDE (0)
2018-11-29 22:36:26,798 - stegosaurus - DEBUG - RETURN_VALUE (0)
2018-11-29 22:36:26,798 - stegosaurus - DEBUG - BINARY_MULTIPLY (0)
2018-11-29 22:36:26,798 - stegosaurus - DEBUG - RETURN_VALUE (0)
2018-11-29 22:36:26,798 - stegosaurus - DEBUG - BINARY_ADD (0)
2018-11-29 22:36:26,798 - stegosaurus - DEBUG - RETURN_VALUE (0)
2018-11-29 22:36:26,798 - stegosaurus - DEBUG - POP_TOP (0)
2018-11-29 22:36:26,798 - stegosaurus - DEBUG - RETURN_VALUE (0)
2018-11-29 22:36:26,798 - stegosaurus - INFO - Found 14 bytes available for payload
Payload embedded in carrier
2018-11-29 22:36:26,799 - stegosaurus - DEBUG - POP_TOP (65) ----A
2018-11-29 22:36:26,799 - stegosaurus - DEBUG - POP_TOP (66) ----B
2018-11-29 22:36:26,799 - stegosaurus - DEBUG - POP_TOP (67) ----C
2018-11-29 22:36:26,799 - stegosaurus - DEBUG - RETURN_VALUE (68) ----D
2018-11-29 22:36:26,799 - stegosaurus - DEBUG - BINARY_SUBTRACT (69) ----E
2018-11-29 22:36:26,799 - stegosaurus - DEBUG - RETURN_VALUE (0)
2018-11-29 22:36:26,799 - stegosaurus - DEBUG - BINARY_TRUE_DIVIDE (0)
2018-11-29 22:36:26,799 - stegosaurus - DEBUG - RETURN_VALUE (0)
2018-11-29 22:36:26,799 - stegosaurus - DEBUG - BINARY_MULTIPLY (0)
2018-11-29 22:36:26,799 - stegosaurus - DEBUG - RETURN_VALUE (0)
2018-11-29 22:36:26,799 - stegosaurus - DEBUG - BINARY_ADD (0)
2018-11-29 22:36:26,799 - stegosaurus - DEBUG - RETURN_VALUE (0)
2018-11-29 22:36:26,799 - stegosaurus - DEBUG - POP_TOP (0)
2018-11-29 22:36:26,799 - stegosaurus - DEBUG - RETURN_VALUE (0)
2018-11-29 22:36:26,799 - stegosaurus - DEBUG - Creating new carrier file name for side-by-side install
2018-11-29 22:36:26,799 - stegosaurus - INFO - Wrote carrier file as __pycache__/example.cpython-36-stegosaurus.pyc
Challenges: WHCTF-2017:Py-Py-Py