【Python 資安筆記】編碼(Encoding)

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 數值。

以下程式碼就做了兩個函數之間的轉換關係:

1
2
3
4
5
6
7
8
9
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:

1
2
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 程式碼實作出兩個函式的轉換關係:

1
2
3
4
5
6
7
8
9
10
# 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 程式碼範例:

1
2
3
4
import base64
flag = '48656C6C6F20576F726C64' # hex string
flag = bytes.fromhex(flag) # hex string to bytes
print(base64.b64encode(flag)) # base64 encoding

Output:

1
b'SGVsbG8gV29ybGQ='

要轉成 ASCII 字串的話,可以使用 decode('ascii') 轉換。

1
2
3
4
5
6
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:

1
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 範例程式碼,展示兩個方法之間的轉換關係:

1
2
3
4
5
6
7
8
9
10
11
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:

1
2
b'Python'
88482574266222

以下程式碼展示了如何將字串轉成大數:

1
2
3
4
5
from Crypto.Util.number import *

flag = b'XD{Y0U_f1Nd_the_f1ag}'

print(bytes_to_long(flag))

Output:

1
129003106218635378415641025119450797906483319236477

XOR

原本的 OR 運算之真值表是這樣的:

Input 1Input 2Output
000
101
011
111

XOR 運算只有當單一個 1 在輸入端時才會 Output 1,亦即當兩個輸入都是 1 的時候會輸出 0,而非 1:

Input 1Input 2Output
000
101
011
110

在大多數程式語言當中,表示 XOR 會用 ^ 符號表示,另外其數學符號是 ⊕,一個圓裡面有十字。

那這在編碼上有什麼應用呢?如可以將一個字串轉成 ASCII 數值,之後對每個字元的 ASCII 數值做 XOR 運算,即可得到另外一個字串。

以下是一個簡單的範例,要從字串 "Ehaab" 中對其做 XOR 13 的運算,看會得到什麼字串:

1
2
flag = "Ehaab"
print(''.join(chr(ord(c) ^ 13) for c in flag))

Output:

1
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 有個題目:

1
2
3
4
KEY1 = a6c8b6733c9b22de7bc0253266a3867df55acde8635e19c73313
KEY2 ^ KEY1 = 37dcb292030faa90d07eec17e3b1c6d8daf94c35d4c9191a5e1e
KEY2 ^ KEY3 = c1545756687e7573db23aa1c3452a098b71a7fbf0fddddde5fc1
FLAG ^ KEY1 ^ KEY3 ^ KEY2 = 04ee9855208a2cd59091d04767ae47963170d1660df7f56f5faf

利用 XOR 的四大特性去推測出 FLAG。

範例程式碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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:

1
73626960647f6b206821204f21254f7d694f7624662065622127234f726927756d

這邊先用 bytes.fromhex() 把他轉成 bytes 看看:

會發現出現一堆亂碼:code

然後題目沒有給你任何提示說,是用哪一個 bytes 去做 XOR 運算的,所以這邊我們可以嘗試用暴力破解看看,因為完整的 ASCII 256 個字元,直接暴力下去就對了。

範例程式碼:

1
2
3
4
5
6
7
8
9
10
11
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 去暴力破解是不行的,然後請看題目:

1
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

1
2
3
4
from pwn import *
flag = bytes.fromhex('0e0b213f26041e480b26217f27342e175d0e070a3c5b103e2526217f27342e175d0e077e263451150104')

print(xor(flag, 'crypto{'.encode())) # xor() 函數, 可方便的做 xor 運算

Output:

1
b'myXORke+y_Q\x0bHOMe$~seG8bGURN\x04DFWg)a|\x1dTM!an\x7f'

可以發現出現了 myXORke+y 這個字串,如果我們去掉 + 號,讓 myXORkeyflag 做 XOR 看看。

1
2
3
4
5
from pwn import *
flag = bytes.fromhex('0e0b213f26041e480b26217f27342e175d0e070a3c5b103e2526217f27342e175d0e077e263451150104')

print(xor(flag, 'crypto{'.encode()))
print(xor(flag, 'myXORkey'.encode())) # modify

Output:

1
2
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

Day 5 - 編碼 - iT 邦幫忙::一起幫忙解決難題,拯救 IT 人的一天

base64 —- Base16、Base32、Base64、Base85 資料編碼 — Python 3.13.7 說明文件

【Day 5】XOR基礎筆記 - iT 邦幫忙::一起幫忙解決難題,拯救 IT 人的一天