前言
隨著射頻識別技術的發展,射頻卡被廣泛應用在了門禁控制、金融支付、庫存管理等場景。在此背景下,各種安全認證機制應運而生,為保護個人隱私和敏感數據提供了可靠的保障,本文將通過一道 CTF 題目介紹 M1 卡采用的 AES(Advanced Encryption Standard)認證機制,揭示其背后的原理。
注:CTF(Capture The Flag)是網絡安全愛好者之間進行技能切磋的比賽,比賽將設置一系列藏有 “flag” 的題目環境,參賽者運用自己的網絡安全技能尋找藏匿的 “flag” 來證明他們攻克了特定環境的題目
前置知識
1、如果一個讀卡器周圍有多張卡,讀卡器的電磁場激活卡片后,卡片會把自己的 UID 回復給讀卡器,讀卡器根據 UID 選擇卡片,然后讀卡器與卡片進行認證,通過之后才進行正常的數據傳輸,避免信息的混肴和丟失
2、ISO/IEC 14443-3 是一項國際標準,主要涉及近場通訊(NFC)和射頻識別(RFID)技術中用于接觸式集成電路卡(IC卡)和讀卡器之間通信的協議標準,在 14443-3 中讀卡器(近距離耦合設備)簡寫為 PCD,卡片(近距離集成電路卡片)簡寫為 PICC
2022 DCTF SecureCard
我們將通過 2022 DCTF SecureCard 這道題目來學習 M1 卡的 AES 認證機制,題目我在這里也提供一份附件:
https://developer.qcloudimg.com/user/attachment/6858803/20230808-832ae4a0.zip
題目著手點
拿到題目根據后綴名 .trace 結合題目名稱 SecureCard 可以推斷出這是通過 pm3 嗅探得到的卡片與讀卡器之間的 trace 文件,那么我們便可以通過 pm3 的客戶端加載這個 trace 文件,看一下內容是什么
pm3簡易使用指南
沒有下載過 pm3 的朋友可以去下載官方提供的編譯好的程序,直接雙擊 pm3.bat 即可打開

pm3 功能對應的命令特別多,但我們通過 help 查看幫助可以很快的找到我們需要的命令,可以看到在第一級菜單里面就有 trace 命令

我們直接輸入 trace 就可以看到這條命令都有什么操作了,很明顯,我們需要從文件里面導入 trace,因此我們應該使用 trace load 命令

輸入 trace load 后因為缺少必要的參數,pm3 會貼心的給我們貼出用法,并給出示例,根據示例,我們可以使用 trace load -f card.trace 來把題目附件加載進來

加載了 441 bytes,但是問題來了,我們該怎么看加載進來的 trace 文件呢,在 [?] 這一行已經有提示了,使用 trace list -1 -t 來查看 trace

那么我們執行發現報錯了,pm3 提示我們缺少一個必要的參數,建議我們使用 --help 看一下幫助文檔

運行幫助發現命令后面需要加一個解析方式,例如 raw 就是僅展示原始數據,不帶注釋

我們也不知道該選什么的,那就都解析一遍看看效果吧,經過不斷的嘗試,發現使用命令 trace list -1 -t des 按照 MIFARE DESFire 解析的時候解析的最好,注釋最全

我摘出來重要部分通過代碼框列為文字版本方便查看
Src | Data (! denotes parity error) | CRC | Annotation
-----+-------------------------------------------------------------------------+-----+--------------------
Rdr |52 | | WUPA
Tag |44 03 | |
Rdr |93 20 | | ANTICOLL
Tag |88 04 29 44 e1 | |
Rdr |93 70 88 04 29 44 e1 a1 33 | ok | SELECT_UID
Tag |24 d8 36 | |
Rdr |95 20 | | ANTICOLL-2
Tag |d2 db 6b 80 e2 | |
Rdr |95 70 d2 db 6b 80 e2 b8 34 | ok | SELECT_UID-2
Tag |20 fc 70 | |
Rdr |e0 80 31 73 | ok | RATS
Tag |06 75 77 81 02 80 02 f0 | ok |
Rdr |0a 00 5a 37 13 00 57 a2 | ok | SELECT APPLICATION (appId 001337)
Tag |0a 00 00 6e d6 | |
Rdr |0b 00 aa 00 9a c4 | ok | AUTH AES (keyNo 0)
Tag |0b 00 af a7 18 45 be 52 8a 7e 8e 08 16 3d 06 3d 95 42 | |
|aa c0 b6 | ok |
Rdr |0a 00 af 2c 2a bd a6 a1 f9 df f5 0b 87 37 6c 30 57 5b | |
|c3 0e 62 4f cd f6 6f 04 0a 3c a1 65 47 47 e2 81 47 28 | |
|b8 | ok | AUTH FRAME / NEXT FRAME
Tag |0a 00 00 60 f9 01 97 5a 30 25 78 5c 0d 43 70 8a de 38 | |
|b2 de b2 | ok |
Rdr |0b 00 f5 01 2c 85 | ok | GET FILE SETTINGS (fileId 01)
Tag |0b 00 00 00 03 00 00 19 00 00 18 6b 65 df 80 ba c2 87 | |
|9d be | ok |
Rdr |0a 00 bd 01 00 00 00 00 00 00 ff 7c | ok | READ DATA (fileId 01, offset 0, len 0)
Tag |0a 00 00 4c be b5 2c 49 15 35 0e af b5 dc fc a9 52 d9 | |
|50 99 4c 12 a1 cf 07 09 82 33 99 57 b4 40 a1 0a 36 01 | |
|7c | ok |
既然是 14443-3 定義的標準,那么我們就從標準入手,逐行分析嗅探數據了解整個交互過程
通信過程分析(卡片選擇)
數據 52 表示 WUPA,是喚醒 Type A 卡的指令(同理 WUPB 是喚醒 Type B 的),卡片收到 WUPA 后會回復 ATQA 告訴讀卡器是否遵守面向比特的防沖突機制,這里回復的是 44 03,但 ISO14443 規定的傳輸方式是首先傳輸低位,所以實際值是 03 44,格式如下:

實際對應到 ATQA 編碼的表中為:
注:RFU 全為 0,Bit frame anticollision 中只有一個為 1 即可

UID size 表示 UID 的長度,"00" 為 4 字節,"01" 為 7 字節,"10" 為 10 字節:
b8
b7
含義
0 0 UID大小:單個 0 1 UID大小:double 1 0 UID大小:三倍 |
接下來進入防沖突循環,讀卡器此時并不知道卡片的 UID,因此讀卡器發送 93 20,其中 SEL 是 93,NVB 是 20
SEL
NVB
93 20 |
SEL 的 93 表示的是 Select cascade level 1

NVB 的 20 表示有效位數。高四位被稱為:字節計數,是讀卡器發送的所有有效數據位的數量除以 8 的整數部分,低四位被稱為:位計數,是讀卡器發送的所有有效數據的數量模 8
b8
b7
b6
b5
含義
0 0 1 0 字節計數=2 0 0 1 1 字節計數=3 0 1 0 0 字節計數=4 0 1 0 1 字節計數=5 0 1 1 0 字節計數=6 0 1 1 1 字節計數=7 |
b4
b3
b2
b1
含義
0 0 0 0 位計數=0 0 0 0 1 位計數=1 0 0 1 0 位計數=2 0 0 1 1 位計數=3 0 1 0 0 位計數=4 0 1 0 1 位計數=5 0 1 1 0 位計數=6 0 1 1 1 位計數=7 |
這時候所有收到的卡片應該回復自己的 UID:88 04 29 44 e1,格式如下:
UID
BCC
88 04 29 44 e1 |
其中 UID 又分為 CT(88)和 UID_CLn(042944)讀卡器收到之后把 NVB 設置為 70,然后選擇卡片 93 70 88 04 29 44 e1 a1 33
SEL
NVB
UID
BCC
CRC
93 70 88 04 29 44 e1 a1 33 |
然后卡片向讀卡器回復 SAK (24 d8 36)
24 的高八位 表示是否符合14443-4
24 的低八位 表示 UID 是否完成
CRC
0010 符合 0100 未完成 36D8 |
注意這里的符合與否,僅看一位二進制位即可,例如是否符合 14443-4 僅看高八位的第三位是否為 1,為 1 則表示符合,為 0 則表示不符合;是否完成僅看低八位的第二位是否為 0,為 0 則表示完成,為 1 則表示未完成
這里的 UID 未完成所以繼續防沖突循環,選擇卡片,95 20 表示 ANTICOLL-2
SEL
NVB
95 20 |
然后得到 UID:d2 db 6b 80 e2
UID
BCC
d2 db 6b 80 e2 |
這時候再次選擇卡片:95 70 d2 db 6b 80 e2 b8 34
SEL
NVB
UID
BCC
CRC
95 70 d2 db 6b 80 e2 34b8 |
這次卡片回復了讀卡器的 SAK 就表示完成了:20 fc 70
20 的高八位 表示是否符合14443-4
20 的低八位 表示 UID 是否完成
CRC
0010 符合 0000 完成 70fc |
然后讀卡器發送 RATS 獲取一些具體類型和配置參數什么的:e0 80 31 73
FSDI(Frame Size Diversification Identifier):FSDI字段指示讀卡器請求的最大幀大小。它決定了讀卡器和智能卡之間數據交換的最大幀大小
CID(Card Identifier):CID字段用于標識智能卡。讀卡器可以使用CID來區分與之通信的多個智能卡
Start byte
FSDI
CID
CRC
e0 8 0 7331 |
卡片回復 ATS :06 75 77 81 02 80 02 f0 這里主要是一些傳輸速率、時鐘頻率等信息就不多介紹了
通信過程分析(AES認證)
下面的嗅探數據都是 0a 00 和 0b 00,通過 pm3 的解析可以看出來,首先選擇了一個 APPLICATION(appId 001337)
Rdr |0a 00 5a 37 13 00 57 a2 | ok | SELECT APPLICATION (appId 001337) Tag |0a 00 00 6e d6 | |
然后開始進行 AES 認證,經過兩次交互完成認證
Rdr |0b 00 aa 00 9a c4 | ok | AUTH AES (keyNo 0)
Tag |0b 00 af a7 18 45 be 52 8a 7e 8e 08 16 3d 06 3d 95 42 | |
|aa c0 b6 | ok |
Rdr |0a 00 af 2c 2a bd a6 a1 f9 df f5 0b 87 37 6c 30 57 5b | |
|c3 0e 62 4f cd f6 6f 04 0a 3c a1 65 47 47 e2 81 47 28 | |
|b8 | ok | AUTH FRAME / NEXT FRAME
Tag |0a 00 00 60 f9 01 97 5a 30 25 78 5c 0d 43 70 8a de 38 | |
|b2 de b2 | ok |
認證結束后選擇了一個文件的設置并讀取了其中的的數據,我們要尋找的 flag 就在 READ DATA 的回復中
Rdr |0b 00 f5 01 2c 85 | ok | GET FILE SETTINGS (fileId 01)
Tag |0b 00 00 00 03 00 00 19 00 00 18 6b 65 df 80 ba c2 87 | |
|9d be | ok |
Rdr |0a 00 bd 01 00 00 00 00 00 00 ff 7c | ok | READ DATA (fileId 01, offset 0, len 0)
Tag |0a 00 00 4c be b5 2c 49 15 35 0e af b5 dc fc a9 52 d9 | |
|50 99 4c 12 a1 cf 07 09 82 33 99 57 b4 40 a1 0a 36 01 | |
|7c | ok |
從 nfc-tools 源碼 中可以看到 mifare_desfire 的認證方法調用了 mifare_desfire_session_key_new 函數來生成 session_key 作為加密密鑰,同時通過 pm3 的源碼可以發現初始向量和密鑰都是全為 0 的
這里對源碼進行閱讀后添加了部分注釋
static int
authenticate(FreefareTag tag, uint8_t cmd, uint8_t key_no, MifareDESFireKey key)
{
int rc;
ASSERT_ACTIVE(tag); //檢查卡片書否處于激活狀態
memset(MIFARE_DESFIRE(tag)->ivect, 0, MAX_CRYPTO_BLOCK_SIZE);
MIFARE_DESFIRE(tag)->authenticated_key_no = NOT_YET_AUTHENTICATED; //設置初始化向量
free(MIFARE_DESFIRE(tag)->session_key); //重置一些標簽的屬性,包括認證狀態、會話密鑰等
MIFARE_DESFIRE(tag)->session_key = NULL;
MIFARE_DESFIRE(tag)->authentication_scheme = (AUTHENTICATE_LEGACY == cmd) ? AS_LEGACY : AS_NEW; //根據傳進來的參數選擇加密方式
BUFFER_INIT(cmd1, 2); //創建緩沖區
BUFFER_INIT(res, 17);
BUFFER_APPEND(cmd1, cmd);
BUFFER_APPEND(cmd1, key_no);
if ((rc = MIFARE_DESFIRE_TRANSCEIVE(tag, cmd1, __cmd1_n, res, __res_size, &__res_n)) < 0)
return rc; //發送認證命令給卡片
size_t key_length = __res_n - 1;
uint8_t PICC_E_RndB[16];
memcpy(PICC_E_RndB, res, key_length); //把卡片產生的的隨機數保存到PICC_E_RndB
uint8_t PICC_RndB[16];
memcpy(PICC_RndB, PICC_E_RndB, key_length); //又把PICC_E_RndB保存到PICC_RndB
mifare_cypher_blocks_chained(tag, key, MIFARE_DESFIRE(tag)->ivect, PICC_RndB, key_length, MCD_RECEIVE, MCO_DECYPHER); //這是對傳過來的隨機數進行解密,得到真的隨機數
uint8_t PCD_RndA[16];
RAND_bytes(PCD_RndA, 16); //本地產生PCD隨機數
uint8_t PCD_r_RndB[16];
memcpy(PCD_r_RndB, PICC_RndB, key_length); //將卡片的隨機數明文復制到PCD_r_RndB
rol(PCD_r_RndB, key_length); //將卡片隨機數進行左移
uint8_t token[32];
memcpy(token, PCD_RndA, key_length);
memcpy(token + key_length, PCD_r_RndB, key_length); //將PCD隨機數和PICC隨機數拼接起來
//下面對拼起來的隨機數進行了加密操作
mifare_cypher_blocks_chained(tag, key, MIFARE_DESFIRE(tag)->ivect, token, 2 * key_length, MCD_SEND, (AUTHENTICATE_LEGACY == cmd) ? MCO_DECYPHER : MCO_ENCYPHER);
BUFFER_INIT(cmd2, 33);
BUFFER_APPEND(cmd2, 0xAF);
BUFFER_APPEND_BYTES(cmd2, token, 2 * key_length);
if ((rc = MIFARE_DESFIRE_TRANSCEIVE(tag, cmd2, __cmd2_n, res, __res_size, &__res_n)) < 0) //發送PCD和PICC拼接起來加密后的隨機數
return rc;
uint8_t PICC_E_RndA_s[16];
memcpy(PICC_E_RndA_s, res, key_length); //再把卡片響應的內容放到PICC_E_RndA_s
uint8_t PICC_RndA_s[16];
memcpy(PICC_RndA_s, PICC_E_RndA_s, key_length);//并解密卡片相應的數據
mifare_cypher_blocks_chained(tag, key, MIFARE_DESFIRE(tag)->ivect, PICC_RndA_s, key_length, MCD_RECEIVE, MCO_DECYPHER);
uint8_t PCD_RndA_s[key_length];
memcpy(PCD_RndA_s, PCD_RndA, key_length); //
rol(PCD_RndA_s, key_length); //PCD隨機數和PICC隨機數拼接起來的那個隨機數循環左移
if (0 != memcmp(PCD_RndA_s, PICC_RndA_s, key_length)) { //將解密完成的PCD_RndA_s和讀卡器自己左移的隨機數對比
#ifdef WITH_DEBUG
hexdump(PCD_RndA_s, key_length, "PCD ", 0);
hexdump(PICC_RndA_s, key_length, "PICC ", 0);
#endif
errno = EACCES;
return -1; //不對的話就退出了
}
MIFARE_DESFIRE(tag)->authenticated_key_no = key_no;
MIFARE_DESFIRE(tag)->session_key = mifare_desfire_session_key_new(PCD_RndA, PICC_RndB, key); //對的話根據兩個隨機數生成密鑰
memset(MIFARE_DESFIRE(tag)->ivect, 0, MAX_CRYPTO_BLOCK_SIZE);
switch (MIFARE_DESFIRE(tag)->authentication_scheme) {
case AS_LEGACY:
break;
case AS_NEW:
cmac_generate_subkeys(MIFARE_DESFIRE(tag)->session_key);
break;
}
return 0;
}
其中生成的 session_key 主要由前兩次交換的隨機數生成,生成的方法是 a[:4] + b[:4] + a[12:16] + b[12:16] ,a 是 PCD(讀卡器)的隨機數,b 是 PICC(卡片)的隨機數
case MIFARE_KEY_AES128: memcpy(buffer, rnda, 4); memcpy(buffer + 4, rndb, 4); memcpy(buffer + 8, rnda + 12, 4); memcpy(buffer + 12, rndb + 12, 4); key = mifare_desfire_aes_key_new(buffer);
整體流程梳理如下

對應于嗅探的數據中,a71845be528a7e8e08163d063d9542aa 是卡片生成的隨機數 b 加密后的數據
2c2abda6a1f9dff50b87376c30575bc30e624fcdf66f040a3ca1654747e28147 是讀卡器生成的隨機數 a 和隨機數 b 拼接起來加密后的數據
60f901975a3025785c0d43708ade38b2 是卡片加密隨機數 a 后的數據
最終生成的 session_key 就是把兩個隨機數明文拼接一下 010203044e71b50c131415160e816b38
我們通過 python 實現一下這個過程就是:
from Crypto.Cipher import AES
key = b'\x00'*16 #定義初始key和iv
iv = b'\x00'*16
encrypt = AES.new(key, AES.MODE_CBC, iv=iv) #定義了一個加密方法
decrypt = AES.new(key, AES.MODE_CBC, iv=iv) #定義了一個解密方法
b = b'Nq\xb5\x0c\xf7Z\xde\xe4\xda;\x11=\x0e\x81k8' #定義明文隨機數b
card_encrypted_b = encrypt.encrypt(b) #將明文b加密為card_encrypted_b
assert card_encrypted_b == bytes.fromhex('a71845be528a7e8e08163d063d9542aa') #檢查加密后的b是否與嗅探到的數據相同
assert b == decrypt.decrypt(card_encrypted_b) #將密文解密對應了認證過程中的讀卡器解密卡片發過來的隨機數b
a = b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16' #讀卡器生成明文隨機數a
reader_encrypted_a_plus_b1 = encrypt.encrypt(a + b[1:]+ bytes([b[0]])) #讀卡器將隨機數a和左移后的隨機數b進行拼接
#這里判斷一下拼接加密后的數據是不是和嗅探到的數據相同
assert reader_encrypted_a_plus_b1 == bytes.fromhex('2c2abda6a1f9dff50b87376c30575bc30e624fcdf66f040a3ca1654747e28147')
a_plus_b1 = decrypt.decrypt(reader_encrypted_a_plus_b1) #將嗅探到的數據進行解密,對應卡片解密讀卡器的數據
b1 = a_plus_b1[16:] #從加密后解密的數據中提取了隨機數b的部分
assert b == bytes([b1[-1]])+b1[:-1] #移位還原判斷是不是b的明文,這里對應了卡片確認讀卡器加密的隨機數b是否正確
card_encrypted_a1 = encrypt.encrypt(a[1:] + bytes([a[0]])) #卡片左移隨機數a加密發送給讀卡器
assert card_encrypted_a1 == bytes.fromhex('60f901975a3025785c0d43708ade38b2') #判斷一下加密后的數據是不是和嗅探到的數據相同
a1 = decrypt.decrypt(card_encrypted_a1) #對應讀卡器解密隨機數a
assert a == bytes([a1[-1]])+a1[:-1] #判斷是否是自己生成的隨機數a
session_key = a[:4] + b[:4] + a[12:16] + b[12:16] #生成session_key
print("session key:",session_key.hex())
但是問題來了,通過這個 key 和全 0 的 IV 解密最后的數據只有一部分 flag
b'\x98{\xa4WY {\xba\xbf}<@\x90\xb9\xb3\x06pl1c4t3d}V\xb7\xd5S\x80\x00\x00'
肯定是 IV 出問題了,閱讀源碼發現:盡管每次認證都會重置 IV 為全 0,但是后續使用時沒有再重置過 IV 了,所以 IV 可能變為了一個未知數,每次調用 mifare_cryto_preprocess_data 時會有一個 cmac 函數把 IV 更新的 cmac 變量中
原 WP 是通過自己編寫了一個 C 語言程序調用 libfreefare 這個庫,不斷地計算不斷地嘗試,最終確定 IV 是 6d52ed2a407e1cb75c276fa4a5981a95 的時候可以解出來 flag
但是我們仔細觀察就會發現,實際上在完成認證之后進行了三次通信,分別傳輸的數據為:F501、0003000019000000、BD01000000000000,我們只需要以這三輪的 data 為參數,以新計算出來的 cmac 為下一輪的 IV 就能得到傳輸 flag 時使用的 IV 了
我不太會用 libfreefare 這個庫,原 WP 給了源碼我也沒跑起來,所以在網上找了很多 python 實現的代碼,最終找到了一個用 python2 實現的代碼:https://gist.github.com/mbenedettini/1409585,稍微一改得到:
from Crypto.Cipher import AES
from bitstring import BitArray
key = BitArray(hex='010203044e71b50c131415160e816b38')
m = bytes.fromhex('F501')
const_rb = BitArray(hex='00000000000000000000000000000087')
k0 = BitArray(hex=AES.new(key.bytes, AES.MODE_CBC, IV=b'\x00'*16).encrypt(b'\x00'*16).hex())
k0_msb = k0[2:][0:1]
if k0_msb == '0':
k1 = k0 << 1
else:
k1 = (k0 << 1) ^ const_rb
print("K0: {k0} \nK1: {k1}".format(k0=k0, k1=k1))
k1_msb = k1[2:][0:1]
if k1_msb == '0':
k2 = k1 << 1
else:
k2 = (k1 << 1) ^ const_rb
print("K2: {k2}".format(k2=k2))
d = BitArray(hex=m.hex())
padded = False
if len(d.bytes) < 16:
padded = True
d.append('0x80')
while len(d.bytes) < 16:
d.append('0x00')
print("d size: %s" % len(d.bytes))
xor_component = None
if padded:
xor_component = BitArray(bytes.fromhex(k2.hex))
else:
xor_component = BitArray(bytes.fromhex(k1.hex))
xored_d = BitArray().join([ d[0:16*8] ^ xor_component , d[16*8:]])
print("xored_d: %s" % xored_d)
ek_xored_d = BitArray()
BLOCK_SIZE = 16 * 8 # Constant
# Split data into 16-byte long pieces
data_blocks = [xored_d[i:i+BLOCK_SIZE] for i in range(0, len(xored_d), BLOCK_SIZE)]
c = AES.new(key.bytes, AES.MODE_CBC, BitArray(hex='00'*16).bytes)
for block in data_blocks:
ek_xored_d.append(BitArray(hex=c.encrypt(block.bytes).hex()))
print("ek_xored_d: %s" % ek_xored_d)
cmac = ek_xored_d[-16*8:]
print("cmac: ", cmac)
復制
將上面的代碼寫成函數的形式,調用三次生成 cmac 后,將 cmac3 作為 IV 去解密就可以得到 flag 了
from Crypto.Cipher import AES
from bitstring import BitArray, Bits
def mifare_desfire_aes_key_new(session_key):
const_rb = BitArray(hex='00000000000000000000000000000087')
IV = b'\x00'*16
m = b'\x00'*16
k0 = BitArray(hex=AES.new(session_key, AES.MODE_CBC, IV=IV).encrypt(m).hex())
#print("K0: {k0}".format(k0=k0))
k0_msb = k0[2:][0:1]
if k0_msb == '0':
k1 = k0 << 1
else:
k1 = (k0 << 1) ^ const_rb
#print("K1: {k1}".format(k1=k1))
k1_msb = k1[2:][0:1]
if k1_msb == '0':
k2 = k1 << 1
else:
k2 = (k1 << 1) ^ const_rb
#print("K2: {k2}".format(k2=k2))
return session_key,bytes.fromhex(k1.hex),bytes.fromhex(k2.hex)
def gen_cmac(key, k1, k2, iv, data):
d = BitArray(hex=data.hex())
padded = False
if len(d.bytes) < 16:
padded = True
d.append('0x80')
while len(d.bytes) < 16:
d.append('0x00')
xor_component = None
if padded:
xor_component = BitArray(bytes.fromhex(k2.hex()))
else:
xor_component = BitArray(bytes.fromhex(k1.hex()))
xored_d = BitArray().join([ d[0:16*8] ^ xor_component , d[16*8:]])
#print("xored_d: %s" % xored_d)
ek_xored_d = BitArray()
BLOCK_SIZE = 16 * 8
data_blocks = [xored_d[i:i+BLOCK_SIZE] for i in range(0, len(xored_d), BLOCK_SIZE)]
c = AES.new(key, AES.MODE_CBC, iv)
for block in data_blocks:
ek_xored_d.append(BitArray(hex=c.encrypt(block.bytes).hex()))
cmac = ek_xored_d[-16*8:]
return bytes.fromhex(cmac.hex)
key = b'\x00'*16 #定義初始key和iv
iv = b'\x00'*16
encrypt = AES.new(key, AES.MODE_CBC, iv=iv) #定義了一個加密方法
decrypt = AES.new(key, AES.MODE_CBC, iv=iv) #定義了一個解密方法
#解出隨機數b
b = decrypt.decrypt(bytes.fromhex('a71845be528a7e8e08163d063d9542aa'))
#解出隨機數a和左移的隨機數b
a_plus_b1 = decrypt.decrypt(bytes.fromhex('2c2abda6a1f9dff50b87376c30575bc30e624fcdf66f040a3ca1654747e28147'))
#解出左移的隨機數a
a1 = decrypt.decrypt(bytes.fromhex('60f901975a3025785c0d43708ade38b2'))
#還原隨機數a
a = bytes([a1[-1]])+a1[:-1]
#生成session_key
session_key = a[:4] + b[:4] + a[12:16] + b[12:16]
key,k1,k2 = mifare_desfire_aes_key_new(session_key)
cmac1 = gen_cmac(key, k1, k2, iv, bytes.fromhex('F501'))
cmac2 = gen_cmac(key, k1, k2, cmac1, bytes.fromhex('0003000019000000'))
#得到最終的IV
cmac3 = gen_cmac(key, k1, k2, cmac2, bytes.fromhex('BD01000000000000'))
#重新創建一個解密方法,使用計算得到的
session = AES.new(session_key, AES.MODE_CBC, iv=cmac3)
flag = session.decrypt(bytes.fromhex('4cbeb52c4915350eafb5dcfca952d950994c12a1cf070982339957b440a10a36'))
print(flag)
復制
最終的 flag 為:dctf{rf1d_15_c0mpl1c4t3d}
b'dctf{rf1d_15_c0mpl1c4t3d}V\xb7\xd5S\x80\x00\x00'
復制
總結和展望
本文通過一道 CTF 題目的了解了 M1 卡的 AES 認證機制,通過針對開源庫的源碼進行逐行分析了解了 AES 認證機制,讀者可自行閱讀源碼分析,并擴展到其他內容
選題思路
隨著射頻識別技術的發展,射頻卡被廣泛應用在了門禁控制、金融支付、庫存管理等場景。在此背景下,各種安全認證機制應運而生,為保護個人隱私和敏感數據提供了可靠的保障。在網絡安全領域攻防一直時不斷對抗不斷進化的,然而對于射頻技術底層原理及認證機制需要研究實現源碼和 ISO 標準,耗時耗力
本文將通過一道 CTF 題目介紹 M1 卡采用的 AES(Advanced Encryption Standard)認證機制,揭示其背后的原理,通過 python 語言快速模擬認證過程,為安全研究人員及網絡安全愛好者提供快速了解 M1 卡 AES 認證機制的方式。同時本文結合開源第三方庫的源碼逐行注釋分析認證及通信過程,也為安全研究人員深入挖掘認證過程漏洞提供的切入點
創作提綱
1、前置知識(主要為沒有了解過 M1 卡機制的讀者簡單介紹卡片與讀卡器之間的通信機制,使文章不管是由經驗還是沒有經驗的朋友都能閱讀)
2、提供題目附件來源(方便讀者一邊閱讀一邊自己動手操作)
3、題目著手點(介紹拿到題目后解題的切入點,有理有據引出下文)
4、pm3簡易使用指南(簡單介紹第三方軟件的使用方法,教讀者讀取題目附件內容)
5、卡片選擇過程分析(結合 ISO 14443 與 pm3 解析的通信過程將通信過程的每一步與 ISO 14443 標準對應起來,解析每個字段的含義,使讀者了解 14443 協議,對于射頻卡機制有一定了解)
6、AES認證過程分析(來到重點內容,結合第三方庫源碼逐行注釋分析 AES 認證機制,并通過 python 自己實現一遍認證過程,讓讀者先了解整個認證過程,并逐步帶領讀者解決存在的問題)
7、總結和展望(總結了整篇分析過程,為后續讀者自行探索提供思路)
安全圈
安全牛
安全內參
安全圈
D1Net
聚銘網絡
安全牛
黑白之道
信息安全與通信保密雜志社
公安部網安局
信息安全國家工程研究中心
HACK之道