Python 安全編碼指南

發表於2015-11-14

0x00 前言

這個pdf中深入Python的核心庫進行分析,並且探討了在兩年的安全程式碼審查過程中,一些被認為是最關鍵的問題,最後也提出了一些解決方案和緩解的方法。我自己也在驗證探究過程中添油加醋了一點,如有錯誤還請指出哈。

下面一張圖表示他們的方法論:

探究的場景為:

  • 輸入的資料是”未知”的型別和大小
  • 使用RFC規範構建Libraries
  • 資料在沒有經過適當的驗證就被處理了
  • 邏輯被更改為是獨立於作業系統的

0x01 Date and time —> time, datetime, os


time

asctime

這裡面asctime()函式是將一個tuple或者是struct_time表示的時間形式轉換成類似於Sun Jun 20 23:21:05 1993的形式,可以time.asctime(time.localtime())驗證一下。對time.struct_time(tm_year=2015, tm_mon=11, tm_mday=7, tm_hour=20, tm_min=58, tm_sec=57, tm_wday=5, tm_yday=311, tm_isdst=0)中每一個鍵值設定invalid_time可造成溢位錯誤。

  • Python 2.6.x中報錯為OverflowError: long int too large to convert to int
  • Python 2.7.x中報錯為
    • OverflowError: Python int too large to convert to C long
    • OverflowError: signed integer is greater than maximum

自己在64位Ubuntu Python2.7.6也測試了一下,輸出結果為:

gmtime

time.gmtime()為將秒數轉化為struct_time格式,它會基於time_t平臺進行檢驗,如上程式碼中將秒數擴大進行測試時會產生報錯ValueError: timestamp out of range for platform time_t。如果數值在-2^63到-2^56之間或者2^55到2^62之間又會引發另一種報錯ValueError: (84, ‘Value too large to be stored in data type’)。我自己的測試結果輸出如下:

os

這裡的os.utime(path, times)是設定對應檔案的access和modified時間,時間以(atime, mtime)元組的形式傳入,程式碼中將modified time設定過大也會產生報錯。

  • Python 2.6.x中報錯為OverflowError: long int too large to convert to int
  • Python 2.7.x, Python 3.1中報錯為OverflowError: Python int too large to convert to C long

如果我們將其中的modified time設定為2^55,ls後會有:

在某些作業系統上如果我們將值設為2^56,將會有以下輸出(也有造成系統崩潰和資料丟失的風險):

Modules通常沒有對無效輸入進行檢查或者測試。例如,對於64位的作業系統,最大數可以達到2^63-1,但是在不同的情況下使用數值會造成不同的錯誤,任何超出有效邊界的數字都會造成溢位,所以要對有效的資料進行檢驗。

0x02 Numbers —> ctypes, xrange, len, decimal

ctype

ctypes是Python的一個外部庫,提供和C語言相容的資料型別,具體可見官方文件

測試程式碼:

舉個例子,可以在64位的作業系統上造成溢位:

Python ctypes 可呼叫的資料型別有:

問題在於:

  • ctypes對記憶體大小沒有限制
  • 沒有對溢位進行檢查

所以,在32位和64位作業系統上都可以造成溢位,解決方案就是也要對資料的有效性和溢位進行檢查。

xrange()

演示程式碼:

報錯為:OverflowError: Python int too large to convert to C long。雖然這種行為是“故意”的和在預期之內的,但在這種情況下依舊沒有進行檢查而導致數字溢位,這是因為xrange使用Plain Integer Objects而無法接受任意長度的物件。解決方法就是使用Python的long integer object,這樣就可以使用任意長度的數字了,限制條件則變為作業系統記憶體的大小了。

len()

演示程式碼:

這裡也會報錯:OverflowError: long int too large to convert to int。因為len()函式沒有對物件的長度進行檢查,也沒有使用python int objects(使用了就會沒有限制),當物件可能包含一個“.length”屬性的時候,就有可能造成溢位錯誤。解決辦法同樣也是使用python int objects。

Decimal

以上程式碼是將Decimal例項和浮點值進行比較,在不同Python版本中如果無法比較則用except捕獲異常,輸出情況為:

  • Python 2.6.5, 2.7.4, 2.7.10中輸出ERROR: FLOAT seems comparable with DECIMAL (WRONG)
  • Python 3.1.2中輸出OK: FLOAT is NOT comparable with DECIMAL (CORRECT)

Type Comparsion

以上程式碼是將字串和浮點值進行比較,在不同Python版本中如果無法比較則用except捕獲異常,輸出情況為:

  • Python 2.6.5, 2.7.4, 2.7.10中輸出ERROR: FLOAT seems comparable with STRING (WRONG)
  • Python 3.1.2中輸出OK: FLOAT is NOT comparable with STRING (CORRECT)

在使用同一種型別的物件進行比較之後,Python內建的比較函式就不會進行檢驗。但在以上兩個程式碼例子當中Python並不知道該如何把STRING和FLOAT進行比較,就會直接返回一個FALSE而不是產生一個Error。同樣的問題也發生於在將DECIMAL和FLOATS時。解決方案就是使用強型別(strong type)檢測和資料驗證。

0x03 Strings —> input, eval, codecs, os, ctypes


eval()

關於eval()函式,Python中eval帶來的潛在風險這篇文章也有提到過,使用__import__匯入os,再結合eval()就可以執行命令了。只要使用者載入瞭直譯器就可以沒有限制地執行任何命令

input()

在以上的程式碼中input()會接受原始輸入,如何這裡使用者傳入一個dir()再結合print,就會執行dir()的功能返回一個物件的大部分屬性:

我在這裡看到了有一個Secret物件,然後藉助原來程式的功能就可以得到該值:

codecs

以上的程式碼將x41xF5x42x43xF4以二進位制的形式寫入檔案,再分別用codecsio模組進行讀取,編碼形式為utf-8,對xF5xF4不能編碼的設定errors='replace',編碼成為\ufffd,最後結果如下:

codecs在讀取x41xF5x42x43xF4這個字串的時候,它期望接收到包含4個位元組的序列,而且因為在讀入xF4的時候它還會再等待其他3個位元組,而沒有進行編碼,結果就是得到的字串有一段被刪除了。更好且安全的方法就是使用os模組,讀取整個資料流,然後進行解碼處理。解決方案就是使用io模組或者對字串進行識別和確認來檢測畸形字元。

os

在不同的平臺上,環境變數名的名稱和語法都是基於不同的規則。但Python並遵守同樣的邏輯,它儘量使用一種普遍的介面來相容大多數的作業系統。這種重視相容性大於安全的選擇,使得用於環境變數的邏輯存在缺陷。

上面的程式碼使用env -i以一個空的環境開始,再設定一個鍵為空值為value的環境變數,使用python列印出來再刪除。這樣就可以定義一個鍵為空的環境變數了,也可以設定在鍵名中包含”=”,但是會無法移除它:

根據不同的版本,Python也會有不同的反應:

  • Python 2.6 —> NO ERRORS,允許無效操作!
  • PYTHON 2.7 —> OSError: [Errno 22] Invalid argument
  • PYTHON 3.1 —> NO ERRORS,允許無效操作!

解決方案是對基礎設施和作業系統進行檢測,檢測和環境變數相關的鍵值對,阻止一些對作業系統為空或者無效鍵值對的使用。

ctypes

ctypes模組在包含空字元的字串中會產生截斷,上面程式碼輸出如下:

這一點和C處理字串是一樣的,會把空字元作為一行的終止。Python在這種情況下使用ctypes,就會繼承相同的邏輯,所以字串就被截斷了。解決方案就是對資料進行確認,刪除字串中的空字元來保護字串或者是禁止使用ctypes

Python Interpreter

以上的測試程式碼應該返回一個語法錯誤:SyntaxError: ‘yield’ outside function。在不同版本的Python上執行結果如下:

這個問題在最新的Python 2.7.x版本中已經解決,而且避免使用像”if 0:“,”if False:“,”while 0:“,”while False:“之類的結構。

0x04 Files —> sys, os, io, pickle, cpickl


pickle

這裡構造惡意序列化字串,以二進位制的形式寫入檔案中,使用pickle.load()函式載入進行反序列化,還原出原始python物件,從而使用os的system()函式來執行命令”ls -la /“。由於pickle這樣安全的設計,就可以藉此來執行命令了。程式碼輸出結果如下:

  • Linux
  • Mac OS X

pickle / cPickle

在上面的程式碼中,根據使用的Python版本不同,picklecPickle要麼儲存截斷的資料而沒有錯誤要麼就會儲存限制為32bit的部分。而且根據Python在作業系統上安裝時編譯的情況,它會返回在請求隨機資料大小上的錯誤,或者是報告無效引數的OS錯誤:

  • cPickle (debian 7 x64)
  • pickle (debian 7 x64)

解決方案就是執行強大的資料檢測來確保不會執行危險行為,還有即使在64位的作業系統上也要限制資料到32位大小。

File Open

以上程式碼主要是測試各種檔案的開啟模式,其中U是指以統一的換行模式開啟(不贊成使用),各個平臺的測試結果如下:

  • Linux and Mac OS X
  • Windows

INVALID stream operations – Linux / OS X

程式碼在這裡使用fileno()來獲取sys.stdout的檔案描述符,在讀寫後就關閉,之後便無法從標準輸入往標準輸出中傳送資料流了。輸出如下:

  • Python 2.6.5, 2.7.4
  • Python 2.7.10

INVALID stream operations – Windows

在windows上也是類似的,如圖:

解決方案就是file和stream庫雖然不遵循OS規範,但它們使用一個通用的邏輯,有必要為每個OS使用有處理能力的庫,來設定正確的呼叫過程。

File Write

我們在Linux上使用strace python -OOBRttu script.py來檢測Python的寫檔案行為:

在這裡我們想要寫入的字元數目是4 + 1048576 = 1048580,在不同的版本上對呼叫open()和使用io模組進行比較:

  • PYTHON 2.6
    • 呼叫open()的輸出為:

      第一次呼叫的時候被緩衝,不僅僅是寫入了4個字元(abcd),還寫入了4092個x;第2次呼叫總共寫入1044480個x。這樣加起來1044480 + 4096 = 1.048.576,相比1048580就少了4個x。等待5秒就可以解決這個問題,因為作業系統flush了快取。
    • 呼叫io模組的輸出為:

      這樣一切就很正常
  • PYTHON 2.7
    • open()的輸出為:

      在這裡進行了三次呼叫,最後再寫入4個x,保證整體資料的正確性。問題就在於這裡使用了3次呼叫而不是我們預期的2次呼叫。
    • 呼叫io模組則一切正常
  • PYTHON 3.x在Python3中用open()函式和io模組則一切都很正常

在Python2中沒有包含原子操作,核心庫是在使用快取進行讀寫。所以應該儘量去使用io模組。

0x05 Protocols —> socket, poplib, urllib, urllib2


httplib, smtplib, ftplib…

核心庫是獨立於作業系統的,開發者必須要知道如何為每一個作業系統構建合適的通訊通道,而且這些庫將會執行執行那些不安全且不正確的操作

在上面的程式碼中構造了一個HTTP服務端,如果一個客戶端連線進來,再去關閉服務端,Python將不會釋放資源,作業系統也不會釋放socket,引發報錯為socket.error: [Errno 48] Address already in use。可以通過以下程式碼來解決:

解決方案就是每一個協議庫都應該由這樣的庫封裝:為每一個OS和協議都適當地建立和撤銷通訊,並釋放資源

poplib, httplib …

服務端:

客戶端:

以上程式碼當中,首先開啟一個虛擬的服務端,使用客戶端去連線服務端,然後服務端開始傳送空字元,客戶端持續性接收空字元,最後到客戶端記憶體填滿,系統崩潰,輸出如下:

  • 服務端
  • 客戶端
    • Python >= 2.7.9, 3.3
    • Python

解決方案就是如果無法控制檢查資料的型別和大小,就使用Python > 2.7.9’或者’Python > 3.3’的版本

對資料沒有進行限制的庫:

urllib, urllib2

urllib2並沒有合適的邏輯來處理資料流而且每次都會失敗,將上次程式碼執行三次都會得到錯誤的檔案大小的輸出:

如果使用以下的程式碼則會產生正確的輸出:

輸出為:

通過以上的例子可以看出,解決方案為利用作業系統來保證資料流的正確性

已知不安全的庫:

最後,當數百萬人在使用它的時候,永遠不要以為它會一直按你期望的那樣運作,也絕對不要以為在使用它的時候是安全的。

相關文章