在現代軟體開發中,安全性已經不再是事後才考慮的議題,而是必須從設計階段就納入考量的核心需求。Python 作為廣泛使用的程式語言,提供了豐富的函式庫與工具來協助開發者實現各種安全機制。然而單純知道這些工具的存在並不夠,更重要的是理解何時該使用哪種工具,以及如何正確地使用它們來建構真正安全的應用程式。
密碼安全是資訊安全的第一道防線,不當的密碼處理方式可能導致整個系統的淪陷。Python 的 secrets 模組提供了密碼學安全的隨機數生成器,遠比一般的 random 模組更適合用於安全相關的場景。配合 hashlib 模組的雜湊演算法,開發者可以建立強健的密碼驗證機制,確保使用者的認證資訊即使在資料庫洩露的情況下也不會直接曝露。
除了運行時的資料保護,程式碼本身的安全性也越來越受到重視。商業軟體需要保護智慧財產權,防止逆向工程與未授權的修改。Python 作為直譯式語言,原始碼相對容易被檢視,這時程式碼混淆技術就變得重要。透過 pyarmor 等工具,可以大幅提高攻擊者分析程式碼的難度。
本文將從實務角度出發,深入探討 Python 在密碼安全、程式碼防護與網路掃描等資訊安全領域的應用。透過完整的程式碼範例與詳細的解說,協助讀者掌握這些關鍵技術的核心概念與實作方法。
使用 secrets 模組生成安全密碼
Python 標準函式庫提供了兩個可以生成隨機數的模組:random 與 secrets。兩者最關鍵的差異在於安全性,random 模組使用的是 Mersenne Twister 演算法,這是一個偽隨機數生成器,雖然在統計上表現良好,但並不適合安全相關的應用。攻擊者如果知道部分輸出值,可能推測出演算法的內部狀態,進而預測後續的輸出。
secrets 模組則使用作業系統提供的密碼學安全隨機數生成器,在 Linux 系統上是 /dev/urandom,在 Windows 上是 CryptGenRandom。這些隨機數來源經過精心設計,確保即使攻擊者觀察到大量輸出值,也無法預測下一個輸出。因此在生成密碼、安全令牌或任何需要不可預測性的場景中,都應該使用 secrets 模組而非 random 模組。
"""
安全密碼生成工具
這個模組展示如何使用 secrets 模組生成符合安全要求的密碼
包含基本密碼生成與符合複雜度要求的強密碼生成
@author 玄貓(BlackCat)
"""
import secrets
import string
def generate_basic_password(length=16):
"""
生成基本的安全密碼
使用 secrets 模組從字母、數字與標點符號中隨機選取字元
這種方式生成的密碼具有密碼學安全性,但不保證包含所有字元類型
參數:
length: 密碼長度,預設為 16 個字元
回傳:
生成的密碼字串
"""
# 定義密碼可使用的字元集合
# string.ascii_letters 包含所有大小寫英文字母 (a-z, A-Z)
# string.digits 包含所有數字 (0-9)
# string.punctuation 包含常見的標點符號
characters = string.ascii_letters + string.digits + string.punctuation
# 使用串列生成式配合 secrets.choice 隨機選取字元
# secrets.choice 會從給定的序列中密碼學安全地隨機選取一個元素
# join 方法將所有字元連接成一個字串
password = ''.join(secrets.choice(characters) for _ in range(length))
return password
def generate_strong_password(length=16):
"""
生成符合複雜度要求的強密碼
確保密碼至少包含一個小寫字母、一個大寫字母、一個數字與一個特殊字元
這符合大多數系統的密碼複雜度要求
參數:
length: 密碼長度,預設為 16 個字元,最少需要 4 個字元
回傳:
符合複雜度要求的強密碼字串
"""
# 確保密碼長度足夠容納所有必要的字元類型
if length < 4:
raise ValueError('密碼長度必須至少為 4 個字元')
# 初始化密碼字串
password = []
# 確保包含至少一個小寫字母
password.append(secrets.choice(string.ascii_lowercase))
# 確保包含至少一個大寫字母
password.append(secrets.choice(string.ascii_uppercase))
# 確保包含至少一個數字
password.append(secrets.choice(string.digits))
# 確保包含至少一個特殊字元
password.append(secrets.choice(string.punctuation))
# 計算剩餘需要填充的長度
remaining_length = length - 4
# 定義完整的字元集合
all_characters = string.ascii_letters + string.digits + string.punctuation
# 隨機填充剩餘的字元
for _ in range(remaining_length):
password.append(secrets.choice(all_characters))
# 打亂密碼字元的順序
# 避免密碼總是以「小寫-大寫-數字-符號」的固定模式開始
# secrets.SystemRandom 提供密碼學安全的隨機排列
secrets.SystemRandom().shuffle(password)
# 將字元串列轉換為字串
return ''.join(password)
def generate_url_safe_token(length=32):
"""
生成 URL 安全的隨機令牌
適用於密碼重設連結、API 金鑰等需要在 URL 中傳遞的場景
生成的字串只包含 URL 安全字元(A-Z, a-z, 0-9, -, _)
參數:
length: 令牌的位元組長度,實際字元數會更長
回傳:
Base64 編碼的 URL 安全字串
"""
# token_urlsafe 生成 URL 安全的隨機字串
# 參數指定隨機位元組的數量
# 實際回傳的字串長度會因為 Base64 編碼而更長
return secrets.token_urlsafe(length)
# 使用範例
if __name__ == '__main__':
# 生成基本密碼
basic_pwd = generate_basic_password()
print(f'基本密碼: {basic_pwd}')
# 生成強密碼
strong_pwd = generate_strong_password()
print(f'強密碼: {strong_pwd}')
# 生成 URL 安全令牌
url_token = generate_url_safe_token()
print(f'URL 令牌: {url_token}')
密碼生成只是第一步,更重要的是如何安全地儲存這些密碼。絕對不應該以明文形式儲存密碼,即使是加密儲存也存在金鑰管理的風險。業界標準做法是使用單向雜湊函數,配合鹽值來保護密碼。
使用 hashlib 實現密碼雜湊與驗證
雜湊函數是一種單向的數學函數,它可以將任意長度的輸入轉換為固定長度的輸出。單向的特性意味著無法從雜湊值反推原始輸入,這使得雜湊函數非常適合用於密碼儲存。當使用者註冊時,系統將密碼雜湊後儲存;當使用者登入時,系統將輸入的密碼雜湊後與儲存的雜湊值比對。這樣即使資料庫被竊取,攻擊者也無法直接取得使用者的原始密碼。
然而單純的雜湊仍然存在弱點。由於相同的輸入總是產生相同的雜湊值,攻擊者可以使用彩虹表攻擊,預先計算常見密碼的雜湊值來快速破解。解決方案是在雜湊前加入隨機的鹽值,每個使用者使用不同的鹽值,這樣即使兩個使用者使用相同的密碼,儲存的雜湊值也會不同。
"""
密碼雜湊與驗證工具
展示如何使用 hashlib 與鹽值來安全地儲存與驗證密碼
包含檔案完整性驗證的應用
@author 玄貓(BlackCat)
"""
import hashlib
import uuid
import os
def hash_password_with_salt(password):
"""
使用鹽值雜湊密碼
生成隨機鹽值並與密碼一起進行雜湊
鹽值會附加在雜湊結果後面,方便後續驗證
參數:
password: 要雜湊的密碼字串
回傳:
格式為 「雜湊值:鹽值」的字串
"""
# 使用 UUID4 生成隨機的鹽值
# hex 方法將 UUID 轉換為十六進位字串
# 每個使用者都有不同的鹽值,防止彩虹表攻擊
salt = uuid.uuid4().hex
# 將鹽值與密碼組合後進行雜湊
# encode() 將字串轉換為位元組序列
# hashlib.sha256() 建立 SHA-256 雜湊物件
# hexdigest() 以十六進位字串回傳雜湊值
password_hash = hashlib.sha256(
(salt + password).encode('utf-8')
).hexdigest()
# 將雜湊值與鹽值組合儲存
# 使用冒號分隔,方便後續解析
return f'{password_hash}:{salt}'
def verify_password(stored_hash, input_password):
"""
驗證密碼是否正確
將輸入的密碼使用相同的鹽值雜湊後,與儲存的雜湊值比對
參數:
stored_hash: 儲存的雜湊值字串,格式為「雜湊值:鹽值」
input_password: 使用者輸入的密碼
回傳:
密碼正確回傳 True,否則回傳 False
"""
# 分離雜湊值與鹽值
# split(':') 以冒號為分隔符號切割字串
stored_password_hash, salt = stored_hash.split(':')
# 使用相同的鹽值雜湊輸入的密碼
input_password_hash = hashlib.sha256(
(salt + input_password).encode('utf-8')
).hexdigest()
# 比對兩個雜湊值是否相同
# 使用 == 運算子比對字串
return stored_password_hash == input_password_hash
def calculate_file_hash(filepath, algorithm='sha256'):
"""
計算檔案的雜湊值
用於驗證檔案完整性,檢測檔案是否被篡改
支援多種雜湊演算法
參數:
filepath: 檔案路徑
algorithm: 雜湊演算法名稱,預設為 sha256
回傳:
檔案的雜湊值,若檔案不存在則回傳 None
"""
# 檢查檔案是否存在
if not os.path.exists(filepath):
print(f'錯誤:檔案 {filepath} 不存在')
return None
try:
# 建立指定演算法的雜湊物件
# hashlib.new() 可以動態建立不同演算法的雜湊物件
hasher = hashlib.new(algorithm)
# 以二進位模式開啟檔案
# 'rb' 模式表示讀取二進位資料
with open(filepath, 'rb') as file:
# 分塊讀取檔案內容
# 這樣可以處理大型檔案而不會耗盡記憶體
# 每次讀取 65536 位元組 (64 KB)
while True:
chunk = file.read(65536)
# 如果讀取到檔案結尾,chunk 會是空的
if not chunk:
break
# 將讀取的資料塊更新到雜湊物件
hasher.update(chunk)
# 回傳最終的雜湊值
return hasher.hexdigest()
except Exception as error:
print(f'計算檔案雜湊值時發生錯誤: {error}')
return None
# 使用範例
if __name__ == '__main__':
# 密碼雜湊範例
password = 'MySecureP@ssw0rd'
# 註冊時:雜湊密碼並儲存
hashed = hash_password_with_salt(password)
print(f'儲存的雜湊值: {hashed}\n')
# 登入時:驗證密碼
test_password = input('請輸入密碼進行驗證: ')
if verify_password(hashed, test_password):
print('密碼正確!')
else:
print('密碼錯誤!')
# 檔案完整性驗證範例
print('\n檔案完整性驗證:')
test_file = __file__ # 使用當前腳本檔案作為測試
file_hash = calculate_file_hash(test_file)
if file_hash:
print(f'{test_file} 的 SHA-256 雜湊值:')
print(file_hash)
@startuml
!define PLANTUML_FORMAT svg
!theme _none_
skinparam dpi auto
skinparam shadowing false
skinparam linetype ortho
skinparam roundcorner 5
skinparam defaultFontName "Microsoft JhengHei UI"
skinparam defaultFontSize 14
skinparam minClassWidth 100
|使用者註冊|
start
:輸入密碼;
|系統處理|
:生成隨機鹽值\n(UUID);
:組合密碼與鹽值;
:執行 SHA-256 雜湊;
:儲存「雜湊值:鹽值」;
stop
|使用者登入|
start
:輸入密碼;
|系統處理|
:從資料庫讀取\n儲存的雜湊值;
:分離雜湊值與鹽值;
:使用相同鹽值\n雜湊輸入密碼;
if (雜湊值比對?) then (相符)
:登入成功;
else (不符)
:登入失敗;
endif
stop
@enduml使用 pyarmor 進行程式碼混淆
Python 作為直譯式語言,原始碼會以 .py 檔案形式發布,即使編譯成 .pyc 位元碼檔案,也可以相對容易地被反編譯回原始碼。對於商業軟體或包含敏感邏輯的應用程式,這可能造成智慧財產權的風險。程式碼混淆是一種技術手段,透過重新命名變數與函數、插入無用程式碼、改變控制流程等方式,使程式碼難以閱讀與理解,提高逆向工程的難度。
pyarmor 是專為 Python 設計的程式碼混淆工具,它不僅混淆程式碼,還會將 Python 腳本轉換為位元碼並加密,執行時才解密並載入到記憶體中。這種方式提供了比單純混淆更強的保護,攻擊者即使取得 .pyc 檔案也無法直接反編譯。
# 安裝 pyarmor
# 使用 pip 套件管理工具從 PyPI 安裝
pip install pyarmor
# 建立測試檔案
cat > test_program.py << 'EOF'
"""
測試程式
這是一個簡單的 Python 程式,用於展示 pyarmor 的混淆效果
"""
def calculate_price(base_price, tax_rate, discount):
"""
計算商品最終價格
參數:
base_price: 基礎價格
tax_rate: 稅率(例如 0.05 表示 5%)
discount: 折扣(例如 0.1 表示 9 折)
回傳:
最終價格
"""
# 計算稅額
tax_amount = base_price * tax_rate
# 計算折扣後價格
discounted_price = base_price * (1 - discount)
# 計算最終價格(折扣後加稅)
final_price = discounted_price + tax_amount
return final_price
def main():
"""
主程式
"""
# 設定商品資訊
product_name = '測試商品'
base_price = 1000
tax_rate = 0.05
discount = 0.1
# 計算價格
final_price = calculate_price(base_price, tax_rate, discount)
# 顯示結果
print(f'商品名稱: {product_name}')
print(f'原價: ${base_price}')
print(f'稅率: {tax_rate * 100}%')
print(f'折扣: {discount * 100}%')
print(f'最終價格: ${final_price:.2f}')
if __name__ == '__main__':
main()
EOF
# 使用 pyarmor 混淆程式碼
# obfuscate 命令會混淆指定的 Python 檔案
# 混淆後的檔案會存放在 dist 目錄中
pyarmor obfuscate test_program.py
# 檢視混淆後的程式碼
# 會看到程式碼被轉換為難以閱讀的形式
cat dist/test_program.py
# 執行混淆後的程式
# 功能完全正常,但原始碼已被保護
python dist/test_program.py
程式碼混淆雖然提高了逆向工程的難度,但並非絕對安全的保護措施。決心足夠的攻擊者仍然可能透過動態分析、記憶體傾印等方式破解混淆。因此混淆應該作為深度防禦策略的一環,配合其他安全措施如授權驗證、防篡改檢測等,才能提供較完整的保護。
使用 scapy 進行網路掃描
網路安全不僅關乎應用程式層的防護,也包含網路層的監控與掃描。scapy 是一個強大的 Python 函式庫,允許開發者以程式化的方式建構、發送與接收網路封包。這使得它非常適合用於網路探索、安全稽核與滲透測試等場景。
透過 scapy 可以實現各種網路掃描技術,從簡單的 ARP 掃描到複雜的 TCP SYN 掃描。ARP 掃描是一種輕量級的掃描方式,透過發送 ARP 請求來發現區域網路上的活動主機。相較於 ICMP ping 掃描,ARP 掃描更難被防火牆阻擋,因為 ARP 是網路運作的基礎協定。
"""
網路掃描工具
使用 scapy 實現區域網路主機發現
展示如何建構與發送自訂的網路封包
@author 玄貓(BlackCat)
"""
from scapy.all import ARP, Ether, srp
import ipaddress
def scan_network(ip_range):
"""
掃描指定 IP 範圍內的活動主機
使用 ARP 協定來發現區域網路上的裝置
ARP 掃描的優勢是難被防火牆阻擋,因為 ARP 是二層協定
參數:
ip_range: IP 範圍,支援 CIDR 表示法(例如 192.168.1.0/24)
回傳:
包含 IP 位址與 MAC 位址的字典串列
"""
try:
# 驗證 IP 範圍格式
# ipaddress 模組可以解析與驗證 IP 位址與網路
ipaddress.ip_network(ip_range)
except ValueError as error:
print(f'錯誤:無效的 IP 範圍格式 - {error}')
return []
# 建立 ARP 請求封包
# pdst 參數指定目標 IP 範圍
# ARP 請求會詢問「誰擁有這個 IP 位址?」
arp_request = ARP(pdst=ip_range)
# 建立以太網路廣播封包
# dst 設為 ff:ff:ff:ff:ff:ff 表示廣播到區域網路的所有裝置
broadcast = Ether(dst='ff:ff:ff:ff:ff:ff')
# 組合以太網路廣播與 ARP 請求
# / 運算子用於堆疊網路層
# 這會建立一個完整的二層網路封包
packet = broadcast / arp_request
# 發送封包並接收回應
# srp 函數用於發送和接收二層封包
# timeout 參數設定等待回應的時間(秒)
# verbose=0 抑制詳細輸出
# 回傳值是兩個串列的元組:已回應的封包和未回應的封包
answered, unanswered = srp(packet, timeout=3, verbose=0)
# 解析回應並建立裝置列表
devices = []
# 遍歷所有已回應的封包
# answered 是一個串列,每個元素是 (發送的封包, 接收的封包) 的元組
for sent, received in answered:
# 提取回應中的 IP 位址與 MAC 位址
# psrc 是協定來源位址(IP)
# hwsrc 是硬體來源位址(MAC)
devices.append({
'ip': received.psrc,
'mac': received.hwsrc
})
return devices
def display_scan_results(devices):
"""
以格式化方式顯示掃描結果
參數:
devices: 裝置資訊的字典串列
"""
if not devices:
print('未發現任何裝置')
return
print(f'\n發現 {len(devices)} 個裝置:\n')
print(f'{"IP 位址":<20} {"MAC 位址":<20}')
print('-' * 40)
for device in devices:
print(f'{device["ip"]:<20} {device["mac"]:<20}')
# 使用範例
if __name__ == '__main__':
# 定義要掃描的 IP 範圍
# 192.168.1.0/24 表示 192.168.1.0 到 192.168.1.255
target_range = '192.168.1.0/24'
print(f'開始掃描網路範圍: {target_range}')
print('這可能需要幾秒鐘的時間...\n')
# 執行掃描
discovered_devices = scan_network(target_range)
# 顯示結果
display_scan_results(discovered_devices)
@startuml
!define PLANTUML_FORMAT svg
!theme _none_
skinparam dpi auto
skinparam shadowing false
skinparam linetype ortho
skinparam roundcorner 5
skinparam defaultFontName "Microsoft JhengHei UI"
skinparam defaultFontSize 14
skinparam minClassWidth 100
|掃描程式|
start
:輸入 IP 範圍\n(例如 192.168.1.0/24);
:建立 ARP 請求封包;
note right
詢問「誰擁有此 IP?」
end note
:建立以太網路廣播幀;
note right
目標 MAC: ff:ff:ff:ff:ff:ff
end note
:組合封包\n(Ether / ARP);
:廣播到區域網路;
|網路上的裝置|
fork
:裝置 A 檢查 IP;
if (IP 符合?) then (是)
:回傳 ARP 回應;
else (否)
:忽略請求;
endif
fork again
:裝置 B 檢查 IP;
if (IP 符合?) then (是)
:回傳 ARP 回應;
else (否)
:忽略請求;
endif
fork again
:裝置 C 檢查 IP;
if (IP 符合?) then (是)
:回傳 ARP 回應;
else (否)
:忽略請求;
endif
end fork
|掃描程式|
:收集所有回應;
:解析 IP 與 MAC 位址;
:顯示掃描結果;
stop
@enduml網路掃描工具具有雙面性,它可以被安全管理員用於網路清查與安全稽核,也可能被攻擊者用於偵察與資訊收集。因此在使用這類工具時,必須確保已取得適當的授權,並且只在自己管理的網路環境中使用。未經授權的網路掃描在許多國家可能觸犯法律。
從實務角度來看,Python 在資訊安全領域提供了完整的工具鏈。從應用層的密碼安全到程式碼層的混淆防護,再到網路層的掃描監控,開發者可以使用統一的程式語言來實現各種安全機制。這種一致性不僅降低了學習成本,也讓安全工具能夠更容易地整合到現有的 Python 應用程式中。
然而工具本身並不能保證安全,更重要的是建立正確的安全思維。密碼雜湊需要配合適當的鹽值與強大的演算法,程式碼混淆需要配合授權驗證與防篡改機制,網路掃描需要配合完整的安全策略與存取控制。只有將這些技術手段與安全架構設計、威脅建模、持續監控等實務結合,才能建構真正安全可靠的系統。
展望未來,隨著量子運算的發展,現有的密碼學演算法可能面臨挑戰。Python 社群也在積極跟進後量子密碼學的研究,預計未來會有更多支援抗量子攻擊演算法的函式庫出現。在程式碼防護方面,基於硬體的可信執行環境與安全飛地技術也逐漸成熟,可能為 Python 應用提供更強的保護。無論技術如何演進,安全防護的核心原則不變:縱深防禦、最小權限、持續監控與快速回應。