---
# System prepended metadata

title: 【Python 資安筆記】編碼（Encoding）
tags: [CTF, Linux, python, CyberSecurity, Python, Ubuntu, 資訊安全, 資安]

---

【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)