【Python 資安筆記】編碼(Encoding) === [TOC] Cover : https://www.publicdomainpictures.net/en/view-image.php?image=563833&picture=hacking 感謝你點進本篇筆記!該系列筆記主要紀錄學習資安的過程,以及我個人的一些簡單白話解釋,另外也涉及到在學校中上課所學的資安技巧及知識。 若本篇文章有誤,麻煩各位告訴我,這樣才能好讓我進步!謝謝~ 自網站 CryptoHack 進行學習:https://cryptohack.org/ ## ASCII 字元編碼 Python 之間的轉換可以用 `chr()` 以及 `ord()` 兩個函式做到。 以下是對於 `chr()` 以及 `ord()` 兩函式的解釋: - `chr()` 接受十進位或十六進位的數字,回傳值為傳入參數所對應的 ASCII 字元,如傳入參數 `97`,就回傳輸出 `'a'` 這個字元。 - `ord()` 與 `chr()` 相反,它接受一個字元當作參數傳入,回傳對應的 ASCII 數值,或是 Unicode 數值。 以下程式碼就做了兩個函數之間的轉換關係: ```python= flag = [72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100] # ASCII Code To Char, using chr() function print(''.join(chr(c) for c in flag)) flag = ['a', 'b', 'c', 'd', 'e'] # Char To ASCII Code, using ord() function print(' '.join(str(ord(c)) for c in flag)) ``` Output: ``` Hello World 97 98 99 100 101 ``` ### Hexadecimal ASCII Code 先把 ASCII 字元轉成相對應的十進位數字,如 `'a'` 轉成 97。 再把這個十進位數字 97 轉成十六進位數字,即可形成一個十六進位字串。 這邊舉 'Hello World' 當例子: * H (72) → 48 (十六進制) * e (101) → 65 (十六進制) * l (108) → 6C (十六進制) * l (108) → 6C (十六進制) * o (111) → 6F (十六進制) * 空格 (32) → 20 (十六進制) * W (87) → 57 (十六進制) * o (111) → 6F (十六進制) * r (114) → 72 (十六進制) * l (108) → 6C (十六進制) * d (100) → 64 (十六進制) 最後把這些十六進位數字拼起來,就會得到這樣的十六進位字串:`48656C6C6F20576F726C64` 在 Python 中,可以利用 `bytes.fromhex()` 函式把 hex(十六進位)轉換成 bytes 資料型態,即將這串十六進位數字編碼了。 與其相對的方法即為 `.hex()` 方法,再把 bytes 轉回去 hex。 以下是對 `bytes.fromhex()` 函式以及 `.hex()` 方法的解釋: - `bytes.fromhex()` 接受一個字串當作參數(這字串當然就是十六進位字串了),然後會回傳一個 bytes 型態的值,如 `b'Hello World'` 裡面的 b 就表示將字串轉成 bytes。 - `.hex()` 是 bytes 物件的方法,主要把 bytes 轉成純十六進位字串(不含 0x)。 以下 Python 程式碼實作出兩個函式的轉換關係: ```python= # Original hex string flag = '48656C6C6F20576F726C64' # Using bytes.fromhex() convert hex string to bytes. flag = bytes.fromhex(flag) print(flag) # Using .hex() method convert bytes to hex string. flag = flag.hex() print(flag) ``` ## Base64 Base64 也是一種常見的編碼系統。 Base64 是將二進位資料編碼成可顯示 ASCII 字元的方法,以 64 個字元組成的 ASCII 字串來表示二進位資料,其中 4 個字元編碼為 3 個 Bytes。 在 Python 中,有個函式是 `base64.b64encode()`,可以對 Base64 做編碼。 在此之前需要引入 base64 函式庫 `import base64`。 另外這個函式傳入的參數要是 bytes,輸出的東西也會是 bytes,所以要事先將字串轉換成 bytes,這邊就用方法 `encode()` 去轉換成 bytes。 以下是個 Python 程式碼範例: ```python= import base64 flag = '48656C6C6F20576F726C64' # hex string flag = bytes.fromhex(flag) # hex string to bytes print(base64.b64encode(flag)) # base64 encoding ``` Output: ``` b'SGVsbG8gV29ybGQ=' ``` 要轉成 ASCII 字串的話,可以使用 `decode('ascii')` 轉換。 ```python= import base64 flag = '48656C6C6F20576F726C64' # hex string flag = bytes.fromhex(flag) # hex string to bytes flag = base64.b64encode(flag) # base64 encoding flag = flag.decode('ascii') # bytes convert to string print(flag) ``` Output: ``` SGVsbG8gV29ybGQ= ``` ## 字串轉數字 將文字訊息轉換成數字,這樣有益於在密碼系統中做數學運算。如 RSA 密碼系統是基於數學運算的,只能處理數字,但我們要加密的訊息通常是由字元組成的文字,因而需要一個標準方法將文字轉換成數字。 最常見的轉換方法: 1. 取得 ASCII 數值:將每個字元轉換成對應的 ASCII 數值。 2. 轉成十六進位:將每個數值表示為十六進位格式。 3. 串接:將所有十六進位數字連接在一起。 4. 當作一個大數字:將整串十六進位當作一個大的數字來處理。 我們拿 "HELLO" 這個字串來舉例: ASCII 字元值: * H → 72 * E → 69 * L → 76 * L → 76 * O → 79 把這些數值轉換成十六進位來表示:`[0x48, 0x45, 0x4c, 0x4c, 0x4f]`。 串接後的十六進位變這樣:`0x48454c4c4f` 轉成十進位大數:`310400273487`。 若要將十進位大數轉換回去原本的文字訊息,可能稍嫌麻煩,但是沒關係,Python 有個函式庫叫做 PyCryptodome,我們輸入 `pip install pycryptodome` 即可安裝。 那這個函式庫幫我們實現了兩個方法:`bytes_to_long()`、`long_to_bytes()`。 如其名,前者是 bytes 轉大數,後者為大數轉 bytes。 在使用函式前可以用以下的程式碼引入函式庫:`from Crypto.Util.number import *`。 以下是 Python 範例程式碼,展示兩個方法之間的轉換關係: ```python= from Crypto.Util.number import * flag = 88482574266222 # Using long_to_bytes() convert long long number to bytes flag = long_to_bytes(flag) print(flag) # Using bytes_to_long() convert bytes to long long number flag = bytes_to_long(flag) print(flag) ``` Output: ``` b'Python' 88482574266222 ``` 以下程式碼展示了如何將字串轉成大數: ```python= from Crypto.Util.number import * flag = b'XD{Y0U_f1Nd_the_f1ag}' print(bytes_to_long(flag)) ``` Output: ``` 129003106218635378415641025119450797906483319236477 ``` ## XOR 原本的 OR 運算之真值表是這樣的: | Input 1 | Input 2 | Output | | -------- | -------- | -------- | | 0 | 0 | 0 | | 1 | 0 | 1 | | 0 | 1 | 1 | | 1 | 1 | 1 | XOR 運算只有當單一個 1 在輸入端時才會 Output 1,亦即當兩個輸入都是 1 的時候會輸出 0,而非 1: | Input 1 | Input 2 | Output | | -------- | -------- | -------- | | 0 | 0 | 0 | | 1 | 0 | 1 | | 0 | 1 | 1 | | 1 | 1 | 0 | 在大多數程式語言當中,表示 XOR 會用 `^` 符號表示,另外其數學符號是 ⊕,一個圓裡面有十字。 那這在編碼上有什麼應用呢?如可以將一個字串轉成 ASCII 數值,之後對每個字元的 ASCII 數值做 XOR 運算,即可得到另外一個字串。 以下是一個簡單的範例,要從字串 `"Ehaab"` 中對其做 XOR 13 的運算,看會得到什麼字串: ```python= flag = "Ehaab" print(''.join(chr(ord(c) ^ 13) for c in flag)) ``` Output: ``` Hello ``` ### XOR 四大特性 - 交換律(Commutative): - 表示 XOR 的運算順序不重要。 - `A ⊕ B = B ⊕ A` - 結合律(Associative): - 同交換律,XOR 運算順序不重要,也不用去擔心有括號這件事。 - `A ⊕ (B ⊕ C) = (A ⊕ B) ⊕ C` - 恆等式(Identity):` - 表示對 0 做 XOR 還是等同他自己(A)。 - A ⊕ 0 = A` - 自反律(Self-Inverse): - 表示對自己 XOR 會回傳 0。 - `A ⊕ A = 0` 在 CryptoHack 有個題目: ``` KEY1 = a6c8b6733c9b22de7bc0253266a3867df55acde8635e19c73313 KEY2 ^ KEY1 = 37dcb292030faa90d07eec17e3b1c6d8daf94c35d4c9191a5e1e KEY2 ^ KEY3 = c1545756687e7573db23aa1c3452a098b71a7fbf0fddddde5fc1 FLAG ^ KEY1 ^ KEY3 ^ KEY2 = 04ee9855208a2cd59091d04767ae47963170d1660df7f56f5faf ``` 利用 XOR 的四大特性去推測出 FLAG。 範例程式碼: ```python= KEY1 = 0xa6c8b6733c9b22de7bc0253266a3867df55acde8635e19c73313 KEY2_XOR_KEY1 = 0x37dcb292030faa90d07eec17e3b1c6d8daf94c35d4c9191a5e1e KEY2_XOR_KEY3 = 0xc1545756687e7573db23aa1c3452a098b71a7fbf0fddddde5fc1 FLAG_XOR_ALL = 0x04ee9855208a2cd59091d04767ae47963170d1660df7f56f5faf KEY2 = KEY1 ^ KEY2_XOR_KEY1 KEY3 = KEY2 ^ KEY2_XOR_KEY3 ALL_KEY = KEY1 ^ KEY2 ^ KEY3 FLAG = hex(ALL_KEY ^ FLAG_XOR_ALL)[2:] print(bytes.fromhex(FLAG)) ``` 上述程式碼的部分,要求 KEY2,那就在題目給的 KEY2 ^ KEY1 中再 XOR 一次 KEY1,讓 KEY1 消掉,即可獲得 KEY2。KEY3、FLAG 以此類推去做。 ### 暴力搜尋 XOR 一樣是 CryptoHack 的題目,題目給你一個十六進位字串,然後去找 Flag: ``` 73626960647f6b206821204f21254f7d694f7624662065622127234f726927756d ``` 這邊先用 `bytes.fromhex()` 把他轉成 bytes 看看: 會發現出現一堆亂碼:![code](https://hackmd.io/_uploads/rJ8CmV3jle.png) 然後題目沒有給你任何提示說,是用哪一個 bytes 去做 XOR 運算的,所以這邊我們可以嘗試用暴力破解看看,因為完整的 ASCII 256 個字元,直接暴力下去就對了。 範例程式碼: ```python= flag = "73626960647f6b206821204f21254f7d694f7624662065622127234f726927756d" decoded = bytes.fromhex(flag) for key in range(256): result = bytes([b ^ key for b in decoded]) try: text = result.decode('ascii', errors='ignore') if text.startswith('crypto{') and text.endswith('}'): print(f'key: {key}, flag: {text}') except: pass ``` 由於它官方的 flag 是由 `crypto{` 開頭,以及 `}` 結尾,所以這邊可以用到字串方法 `.startswith()` 跟 `.endswith()` 判斷字串開頭即結尾。 若找到就直接輸出該字串,不用看到一大堆亂碼了。 ### 更複雜的 XOR 接下來這個題目用剛剛單一個 bytes 去暴力破解是不行的,然後請看題目: ``` 0e0b213f26041e480b26217f27342e175d0e070a3c5b103e2526217f27342e175d0e077e263451150104 ``` 若用剛剛單一 bytes 去暴力破解,會發現完全找不出有關 `crypto{}` 的資訊,所以唯一可能就是這個 XOR 運算用到多 bytes。 然後 CryptoHack 裡面有提示:"Remember the flag format and how it might help you in this challenge!" 這邊我們試著把 flag 跟 'crypto{' 做 XOR 看看(用 pwntools): pwntools installation in Windows:https://blog.pcat.cc/%E5%AE%89%E8%A3%85%E4%BD%BF%E7%94%A8%E6%96%B9%E6%B3%95/pwn/pwntools ```python= from pwn import * flag = bytes.fromhex('0e0b213f26041e480b26217f27342e175d0e070a3c5b103e2526217f27342e175d0e077e263451150104') print(xor(flag, 'crypto{'.encode())) # xor() 函數, 可方便的做 xor 運算 ``` Output: ``` b'myXORke+y_Q\x0bHOMe$~seG8bGURN\x04DFWg)a|\x1dTM!an\x7f' ``` 可以發現出現了 `myXORke+y` 這個字串,如果我們去掉 + 號,讓 `myXORkey` 跟 `flag` 做 XOR 看看。 ```python= from pwn import * flag = bytes.fromhex('0e0b213f26041e480b26217f27342e175d0e070a3c5b103e2526217f27342e175d0e077e263451150104') print(xor(flag, 'crypto{'.encode())) print(xor(flag, 'myXORkey'.encode())) # modify ``` Output: ``` b'myXORke+y_Q\x0bHOMe$~seG8bGURN\x04DFWg)a|\x1dTM!an\x7f' b'crypto{1f_y0u_Kn0w_En0uGH_y0u_Kn0w_1t_4ll}' ``` 居然,這就是答案了! 解法來自:https://cryptohack.org/challenges/xorkey1/solutions/ 這是一名叫 oushanmu 的 user 的解法。 然後我們在 CryptoHack 中的 Introduction to CryptoHack 環節就完成啦。 ## 總整理 ### ASCII 字元編碼 - `chr()`:數字 → 字元 (如:chr(97) → 'a') - `ord()`:字元 → 數字 (如:ord('a') → 97) ### 十六進位轉換 - `bytes.fromhex()`:十六進位字串 → bytes - `.hex()`:bytes → 十六進位字串 - 範例:"Hello" → ASCII → 十六進位 → "48656C6C6F" 須注意這邊 `.hex()` 是方法,而 `hex()` 是函式,兩者是不同的,後者是轉換成 hex 字串。 ### Base64 編碼 使用 base64 方法前須引入 `import base64` - `base64.b64encode()`:bytes → Base64 字串 - `decode('ascii')`:bytes → 字串 - 用於二進位資料的 ASCII 表示。 ### 大數轉換 (PyCryptodome) 使用下面這些方法前須引入 `from Crypto.Util.number import *` - `bytes_to_long()`:bytes → 大整數 - `long_to_bytes()`:大整數 → bytes - 用於 RSA 等密碼學應用。 ### XOR 運算 四大特性: - 交換律(Commutative): - 表示 XOR 的運算順序不重要。 - `A ⊕ B = B ⊕ A` - 結合律(Associative): - 同交換律,XOR 運算順序不重要,也不用去擔心有括號這件事。 - `A ⊕ (B ⊕ C) = (A ⊕ B) ⊕ C` - 恆等式(Identity):` - 表示對 0 做 XOR 還是等同他自己(A)。 - A ⊕ 0 = A` - 自反律(Self-Inverse): - 表示對自己 XOR 會回傳 0。 - `A ⊕ A = 0` 破解方法: 1. 暴力破解:嘗試 0~255 ASCII Code 去推導出所有的可能 key。 2. 已知明文攻擊:利用已知格式推導 key。 ### 實用工具 - pwntools:`xor()` 函數方便於 XOR 運算。 - `startswith()` / `endswith()`:檢查字串開頭結尾。 ## Reference [CryptoHack – A free, fun platform for learning cryptography](https://cryptohack.org/) [Day 5 - 編碼 - iT 邦幫忙::一起幫忙解決難題,拯救 IT 人的一天](https://ithelp.ithome.com.tw/articles/10375585) [base64 --- Base16、Base32、Base64、Base85 資料編碼 — Python 3.13.7 說明文件](https://docs.python.org/zh-tw/3/library/base64.html) [【Day 5】XOR基礎筆記 - iT 邦幫忙::一起幫忙解決難題,拯救 IT 人的一天](https://ithelp.ithome.com.tw/articles/10318277)