【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 看看:
會發現出現一堆亂碼:
然後題目沒有給你任何提示說,是用哪一個 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)