mirror of
https://github.com/RfidResearchGroup/ChameleonMini.git
synced 2026-05-12 11:20:37 -07:00
Merge pull request #181 from gypsophlia/DecodeMFDPython-pr
Upgrade chamlog tool, Decode Mifare DESFire and remove parity bit
This commit is contained in:
@@ -21,7 +21,10 @@ class Device:
|
||||
COMMAND_RBUTTON = "RBUTTON"
|
||||
COMMAND_GREEN_LED = "LEDGREEN"
|
||||
COMMAND_RED_LED = "LEDRED"
|
||||
|
||||
COMMAND_THRESHOLD = "THRESHOLD"
|
||||
COMMAND_UPGRADE = "upgrade"
|
||||
|
||||
|
||||
STATUS_CODE_OK = 100
|
||||
STATUS_CODE_OK_WITH_TEXT = 101
|
||||
STATUS_CODE_WAITING_FOR_XMODEM = 110
|
||||
@@ -240,3 +243,15 @@ class Device:
|
||||
return self.getCmdSuggestions(self.COMMAND_RED_LED)
|
||||
else:
|
||||
return self.getSetCmd(self.COMMAND_RED_LED, newFunction)
|
||||
|
||||
def cmdThreshold(self, value):
|
||||
if(value == self.SUGGEST_CHAR):
|
||||
return self.getCmdSuggestions(self.COMMAND_THRESHOLD)
|
||||
else:
|
||||
return self.getSetCmd(self.COMMAND_THRESHOLD, value)
|
||||
|
||||
def cmdUpgrade(self):
|
||||
# Execute command
|
||||
cmdLine = self.COMMAND_UPGRADE + self.LINE_ENDING
|
||||
self.serial.write(cmdLine.encode('ascii'))
|
||||
return 0
|
||||
|
||||
@@ -0,0 +1,343 @@
|
||||
import binascii
|
||||
import crcmod
|
||||
from enum import Enum
|
||||
|
||||
from Chameleon.MFDESFire import MFDESFireDecode
|
||||
from Chameleon.utils import TrafficSource
|
||||
|
||||
# Parameters for CRC_A
|
||||
CRC_INIT = 0x6363
|
||||
POLY = 0x11021
|
||||
CRC_A_func = crcmod.mkCrcFun(POLY, initCrc=CRC_INIT, xorOut=0)
|
||||
|
||||
|
||||
class ReaderCMD(Enum):
|
||||
NONE = 0
|
||||
SELECT = 1
|
||||
RATS = 2
|
||||
PPS = 3
|
||||
|
||||
|
||||
readerCMD = ReaderCMD.NONE
|
||||
|
||||
# Map card types string to decoder
|
||||
def DummyCardDecoder(data, source):
|
||||
return ""
|
||||
|
||||
CardTypesMap = {
|
||||
"None": {"ApplicationDecoder": DummyCardDecoder},
|
||||
"MFDESFire": {"ApplicationDecoder": MFDESFireDecode},
|
||||
|
||||
}
|
||||
|
||||
|
||||
class BlockData:
|
||||
@staticmethod
|
||||
def isBlockData(byteCount, data):
|
||||
if(byteCount >= 3 and (data[0]& 0xE6) in ReaderTrafficTypes["PCB"]):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def __init__(self, byteCount, data, source, Cardtype):
|
||||
self.byteCount = byteCount
|
||||
self.data = data
|
||||
self.PCB = data[0]
|
||||
self.type = ReaderTrafficTypes["PCB"][self.PCB & 0xE6]
|
||||
self.CID = None
|
||||
self.NAD = None
|
||||
self.INF = None
|
||||
self.source = source
|
||||
self.CRCChecked = CRC_A_check(data)
|
||||
self.CardApplicationDecoder = CardTypesMap[Cardtype]["ApplicationDecoder"]
|
||||
|
||||
if self.CRCChecked:
|
||||
|
||||
hasCID = self.PCB & 0x08 # PCB b4 indicate CID
|
||||
hasNAD = 0
|
||||
|
||||
if (self.type == "IBlock"):
|
||||
hasNAD = self.PCB & 0x04 # IBlock PCB b2 indicate NAD
|
||||
|
||||
byteNext = 1
|
||||
# CID
|
||||
if (hasCID):
|
||||
self.CID = self.data[byteNext]
|
||||
byteNext += 1
|
||||
|
||||
# NAD
|
||||
if (hasNAD):
|
||||
self.NAD = self.data[byteNext]
|
||||
byteNext += 1
|
||||
|
||||
# INF field not empty
|
||||
if(byteNext < byteCount -2):
|
||||
self.INF = self.data[byteNext: byteCount-2]
|
||||
|
||||
|
||||
def decode(self):
|
||||
note = ""
|
||||
# Prologue
|
||||
# PCB
|
||||
note += self.type + " "
|
||||
|
||||
# Block number
|
||||
if ((self.type == "IBlock" or self.type == "RBlock") and self.PCB & 0x01):
|
||||
note += "BlkNo:1 "
|
||||
|
||||
# Chaining?
|
||||
if (self.type == "IBlock" and self.PCB & 0x10):
|
||||
note += "Chaining "
|
||||
|
||||
# ACK/NAK? for R-Block
|
||||
if (self.type == "RBlock"):
|
||||
if (self.PCB & 0x10):
|
||||
note += "NAK "
|
||||
else:
|
||||
note += "ACK "
|
||||
|
||||
# DESEL/WTX for SBlock
|
||||
if (self.type == "SBlock"):
|
||||
if (self.PCB & 0x30 == 0x00):
|
||||
note += "DESEL "
|
||||
elif (self.PCB & 0x30 == 0x30):
|
||||
note += "WTX"
|
||||
|
||||
# CID
|
||||
if (self.CID != None):
|
||||
note += "CID:" + hex(self.CID) + " "
|
||||
|
||||
# NAD
|
||||
if (self.NAD != None):
|
||||
note += "NAD:" + hex(self.NAD) + " "
|
||||
|
||||
# INF
|
||||
if (self.INF != None):
|
||||
note += self.CardApplicationDecoder(self.INF, self.source)
|
||||
# EDC CRC check
|
||||
if not self.CRCChecked:
|
||||
note += " WRONG CRC "
|
||||
|
||||
|
||||
return note
|
||||
|
||||
|
||||
ReaderTrafficTypes = {
|
||||
"SEL":{
|
||||
0x93: "SEL_CL1 ",
|
||||
0x95: "SEL_CL2 ",
|
||||
0x97: "SEL_CL3 ",
|
||||
},
|
||||
# 1 byte commands
|
||||
"SHORTFRAME": {
|
||||
0x26: "REQA",
|
||||
0x52: "WUPA",
|
||||
0x35: "Optional Timeslot Method",
|
||||
# 40 - 4F Proprietary
|
||||
# 78 - 7F proprietary
|
||||
# Other RFU
|
||||
},
|
||||
"FSDI":{
|
||||
0x0: "FSD:16 ",
|
||||
0x1: "FSD:24 ",
|
||||
0x2: "FSD:32 ",
|
||||
0x3: "FSD:40 ",
|
||||
0x4: "FSD:48 ",
|
||||
0x5: "FSD:64 ",
|
||||
0x6: "FSD:96 ",
|
||||
0x7: "FSD:128 ",
|
||||
0x8: "FSD:256 "
|
||||
},
|
||||
"PCB":{
|
||||
# IBlock 000X XX1X
|
||||
0x02: "IBlock", # 000X X01X
|
||||
0x08: "IBlock", # 000X X11X
|
||||
# RBlock 101X X01X
|
||||
0xA2: "RBlock", # 101X X01X
|
||||
# SBlock 11XX X010
|
||||
0xC2: "SBlock", # 110X X010
|
||||
0xE2: "SBlock" # 111X X010
|
||||
}
|
||||
}
|
||||
|
||||
CardTrafficTypes = {
|
||||
"SAK":{
|
||||
0x04: "UID NOT Complete ",
|
||||
0x24: "UID NOT Complete, PICC compliant with 14443-4",
|
||||
0x20: "UID complete, PICC compliant with 14443-4 ",
|
||||
0x00: "UID complete, PICC NOT compliant with 14443-4"
|
||||
},
|
||||
"FSCI": {
|
||||
0x0: "FSC:16 ",
|
||||
0x1: "FSC:24 ",
|
||||
0x2: "FSC:32 ",
|
||||
0x3: "FSC:40 ",
|
||||
0x4: "FSC:48 ",
|
||||
0x5: "FSC:64 ",
|
||||
0x6: "FSC:96 ",
|
||||
0x7: "FSC:128 ",
|
||||
0x8: "FSC:256 "
|
||||
}
|
||||
}
|
||||
|
||||
def CRC_A(data):
|
||||
return CRC_A_func(data)
|
||||
|
||||
def CRC_A_check(data):
|
||||
datalen = len(data)
|
||||
# Short frame/SAK or no space for CRC skip check
|
||||
if(datalen < 3 ):
|
||||
return True
|
||||
|
||||
crc = CRC_A(bytearray(data[0:datalen-2])).to_bytes(2,'little')
|
||||
if (data[datalen-2:datalen] == crc):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def parseReader_3(data):
|
||||
global readerCMD
|
||||
byteCount = len(data)
|
||||
note = ""
|
||||
|
||||
# short frame commands
|
||||
if (byteCount == 1 and data[0] in ReaderTrafficTypes["SHORTFRAME"]):
|
||||
note += ReaderTrafficTypes["SHORTFRAME"][data[0]]
|
||||
|
||||
# ANTICOLLISION command
|
||||
elif (byteCount < 9 and byteCount > 1 and data[0] in ReaderTrafficTypes["SEL"] and data[1] & 0x88 == 0):
|
||||
note += "ATCOLI - "
|
||||
note += ReaderTrafficTypes["SEL"][data[0]]
|
||||
# note += "UID_CLn:" + binascii.hexlify(data[2:7]).decode() + " "
|
||||
# note += str((data[1] >> 4) & 0x0f) + "bytes + " + str(data[1] & 0x0f) + "bits "
|
||||
|
||||
# SELECT Command
|
||||
elif (byteCount == 9 and data[0] in ReaderTrafficTypes["SEL"] and data[1] == 0x70):
|
||||
readerCMD = ReaderCMD.SELECT
|
||||
note += "SELECT - "
|
||||
note += ReaderTrafficTypes["SEL"][data[0]]
|
||||
note += "UID_CLn:" + binascii.hexlify(data[2:6]).decode() + " "
|
||||
# Check CRC for SELECT
|
||||
if (not CRC_A_check(data)):
|
||||
note += " WRONG CRC "
|
||||
# note += "7bytes + 0bits "
|
||||
# note += "CRC_A:"+data[7:9]
|
||||
# HALT Command
|
||||
elif (byteCount == 4 and data[0] == 0x50 and data[1] == 0x00):
|
||||
note += "HALT"
|
||||
|
||||
return note
|
||||
|
||||
def parseReader_4(data, Cardtype):
|
||||
global readerCMD
|
||||
byteCount = len(data)
|
||||
note = ""
|
||||
|
||||
# RATS
|
||||
if (byteCount == 4 and data[0] == 0xe0 and ((data[1] & 0x0f) < 15) and (
|
||||
(data[1] & 0xf0 >> 8) in ReaderTrafficTypes["FSDI"])):
|
||||
note += "RATS - "
|
||||
note += ReaderTrafficTypes["FSDI"][data[1] >> 8]
|
||||
note += "CID:" + hex(data[1] & 0x0f) + " "
|
||||
if (not CRC_A_check(data)):
|
||||
note += " WRONG CRC "
|
||||
else:
|
||||
readerCMD = ReaderCMD.RATS
|
||||
|
||||
# PPS Protocol and parameter selection request
|
||||
# PSS0 only
|
||||
elif (byteCount == 4 and (data[0] & 0xf0 == 0xd0) and (data[1] == 0x01)):
|
||||
note += "PSS0 - "
|
||||
note += "CID:" + str(data[1] & 0x0f) + " "
|
||||
readerCMD = ReaderCMD.PPS
|
||||
# PSS0+1
|
||||
elif (byteCount == 5 and (data[0] & 0xf0 == 0xd0) and (data[1] == 0x11) and (data[2] & 0xf0 == 0x00)):
|
||||
note += "PSS0+1 - "
|
||||
note += "CID:" + str(data[1] & 0x0f) + " "
|
||||
note += "DSI:" + str(pow(2, (data[2] >> 2) & 0x03)) + " "
|
||||
note += "DRI:" + str(pow(2, data[2] & 0x03)) + " "
|
||||
|
||||
# Half-duplex block transmission
|
||||
# PCB bit mask: 0b11100110
|
||||
elif (BlockData.isBlockData(byteCount, data)):
|
||||
blockData = BlockData(byteCount,data, TrafficSource.Reader, Cardtype)
|
||||
note = blockData.decode()
|
||||
|
||||
return note
|
||||
|
||||
def parseCard_3(data):
|
||||
byteCount = len(data)
|
||||
note = ""
|
||||
|
||||
# ATQA: RRRR XXXX XXRX XXXX
|
||||
if(byteCount == 2 and (data[0] & 0x20 == 0x00) and (data[1] & 0xf0 == 0x00)):
|
||||
note += "ATQA - "
|
||||
note += binascii.hexlify(data).decode()
|
||||
# SAK
|
||||
elif (byteCount == 3 and readerCMD == ReaderCMD.SELECT and ((data[0] & (0x24)) in CardTrafficTypes["SAK"])):
|
||||
note += "SAK - "
|
||||
note += CardTrafficTypes["SAK"][(data[2] & 0x24)]
|
||||
if not CRC_A_check(data):
|
||||
note += " WRONG CRC "
|
||||
# UID
|
||||
elif (byteCount == 5 and (data[0] ^ data[1] ^ data[2] ^ data[3]) == data[4] ):
|
||||
note += "UID Resp - CLn "
|
||||
|
||||
return note
|
||||
|
||||
def parseCard_4(data, Cardtype):
|
||||
global readerCMD
|
||||
byteCount = len(data)
|
||||
note = ""
|
||||
|
||||
# ATS
|
||||
# TL + T0 + TA + TB + TC + T1 ... + CRC
|
||||
# TL: length without CRC
|
||||
# ATS without data
|
||||
if(byteCount == 3 and readerCMD == ReaderCMD.RATS and data[0] == (byteCount-2)):
|
||||
note += "ATS - NO DATA"
|
||||
# ATS with data mush have T0, T0 b8=0
|
||||
elif (byteCount > 3 and readerCMD == ReaderCMD.RATS and data[0] == (byteCount-2)
|
||||
and data[1] & 0x80 == 0x00
|
||||
and data[1] & 0x0f in CardTrafficTypes["FSCI"]):
|
||||
note += "ATS - "
|
||||
# Decode T0
|
||||
hasTA = data[1] & 0x10
|
||||
hasTB = data[1] & 0x20
|
||||
hasTC = data[1] & 0x40
|
||||
note += CardTrafficTypes["FSCI"][data[1] & 0x0f]
|
||||
|
||||
# Which byte to decode next
|
||||
byteNext = 2 # T0 Decoded, next is TA/TB/TC
|
||||
# TA b4=0
|
||||
if (hasTA and data[byteNext] & 0x08 == 0x00):
|
||||
note += "TA:" + hex(data[byteNext]) + " "
|
||||
byteNext += 1
|
||||
|
||||
if (hasTB):
|
||||
note += "TB:" + hex(data[byteNext]) + " "
|
||||
byteNext += 1
|
||||
|
||||
# TC b3-8=0
|
||||
if (hasTC and data[byteNext] & 0xFC == 0x00):
|
||||
note += "TC:" + hex(data[byteNext]) + " "
|
||||
byteNext += 1
|
||||
|
||||
# Check CRC_A
|
||||
if not CRC_A_check(data):
|
||||
note += " WRONG CRC "
|
||||
# Application Data
|
||||
elif (BlockData.isBlockData(byteCount, data)):
|
||||
blockData = BlockData(byteCount,data, TrafficSource.Card, Cardtype)
|
||||
note = blockData.decode()
|
||||
|
||||
readerCMD = ReaderCMD.NONE
|
||||
|
||||
return note
|
||||
|
||||
def parseReader(data, Cardtype):
|
||||
return parseReader_3(data) + parseReader_4(data, Cardtype)
|
||||
|
||||
def parseCard(data, Cardtype):
|
||||
return parseCard_3(data) + parseCard_4(data, Cardtype)
|
||||
@@ -2,6 +2,41 @@
|
||||
|
||||
import struct
|
||||
import binascii
|
||||
import math
|
||||
import Chameleon.ISO14443 as iso14443_3
|
||||
|
||||
def checkParityBit(data):
|
||||
byteCount = len(data)
|
||||
# Short frame, no parityBit
|
||||
if (byteCount == 1):
|
||||
return (True, data)
|
||||
|
||||
# 9 bit is a group, validate bit count is calculated below
|
||||
bitCount = int((byteCount*8)/9) * 9
|
||||
parsedData = bytearray(int(bitCount/9))
|
||||
|
||||
oneCounter = 0 # Counter for count ones in a byte
|
||||
for i in range(0, bitCount):
|
||||
# Get bit i in data
|
||||
byteIndex = math.floor(i/8)
|
||||
bitIndex = i % 8
|
||||
bit = (data[byteIndex] >> bitIndex) & 0x01
|
||||
|
||||
# Check parityBit
|
||||
# Current bit is parityBit
|
||||
if(i % 9 == 8):
|
||||
# Even number of ones in current byte
|
||||
if(oneCounter % 2 and bit == 1):
|
||||
return (False, data)
|
||||
# Odd number of ones in current byte
|
||||
elif((not oneCounter % 2) and bit == 0):
|
||||
return (False, data)
|
||||
oneCounter = 0
|
||||
# Current bit is normal bit
|
||||
else:
|
||||
oneCounter += bit
|
||||
parsedData[int(i/9)] |= bit << (i%9)
|
||||
return (True, parsedData)
|
||||
|
||||
def noDecoder(data):
|
||||
return ""
|
||||
@@ -12,6 +47,13 @@ def textDecoder(data):
|
||||
def binaryDecoder(data):
|
||||
return binascii.hexlify(data).decode()
|
||||
|
||||
def binaryParityDecoder(data):
|
||||
isValid, checkedData = checkParityBit(data)
|
||||
if(isValid):
|
||||
return binascii.hexlify(checkedData).decode()
|
||||
else:
|
||||
return binascii.hexlify(checkedData).decode()+"!"
|
||||
|
||||
eventTypes = {
|
||||
0x00: { 'name': 'EMPTY', 'decoder': noDecoder },
|
||||
0x10: { 'name': 'GENERIC', 'decoder': textDecoder },
|
||||
@@ -26,9 +68,9 @@ eventTypes = {
|
||||
0x43: { 'name': 'CODEC TX W/PARITY', 'decoder': binaryDecoder },
|
||||
|
||||
0x44: { 'name': 'CODEC RX SNI READER', 'decoder': binaryDecoder },
|
||||
0x45: { 'name': 'CODEC RX SNI READER W/PARITY', 'decoder': binaryDecoder },
|
||||
0x45: { 'name': 'CODEC RX SNI READER W/PARITY', 'decoder': binaryParityDecoder },
|
||||
0x46: { 'name': 'CODEC RX SNI CARD', 'decoder': binaryDecoder },
|
||||
0x47: { 'name': 'CODEC RX SNI CARD W/PARITY', 'decoder': binaryDecoder },
|
||||
0x47: { 'name': 'CODEC RX SNI CARD W/PARITY', 'decoder': binaryParityDecoder },
|
||||
|
||||
|
||||
0x80: { 'name': 'APP READ', 'decoder': binaryDecoder },
|
||||
@@ -55,7 +97,7 @@ eventTypes = {
|
||||
TIMESTAMP_MAX = 65536
|
||||
eventTypes = { i : ({'name': 'UNKNOWN', 'decoder': binaryDecoder} if i not in eventTypes.keys() else eventTypes[i]) for i in range(256) }
|
||||
|
||||
def parseBinary(binaryStream):
|
||||
def parseBinary(binaryStream, decoder=None):
|
||||
log = []
|
||||
|
||||
# Completely read file contents and process them byte by byte
|
||||
@@ -94,13 +136,23 @@ def parseBinary(binaryStream):
|
||||
if (deltaTimestamp < 0):
|
||||
deltaTimestamp += TIMESTAMP_MAX;
|
||||
|
||||
note = ""
|
||||
# If we need to decode the data and paritybit check success
|
||||
if (decoder!=None and len(logData) >0 and logData[-1] != '!'):
|
||||
# Decode the data from Reader
|
||||
if(event == 0x44 or event == 0x45):
|
||||
note = iso14443_3.parseReader(binascii.a2b_hex(logData), decoder)
|
||||
elif (event == 0x46 or event == 0x47):
|
||||
note = iso14443_3.parseCard(binascii.a2b_hex(logData), decoder)
|
||||
|
||||
# Create log entry as dict and append it to event list
|
||||
logEntry = {
|
||||
'eventName': eventTypes[event]['name'],
|
||||
'dataLength': dataLength,
|
||||
'timestamp': timestamp,
|
||||
'deltaTimestamp': deltaTimestamp,
|
||||
'data': logData
|
||||
'data': logData,
|
||||
'note': note
|
||||
}
|
||||
|
||||
log.append(logEntry)
|
||||
|
||||
@@ -0,0 +1,372 @@
|
||||
from Chameleon.utils import TrafficSource
|
||||
from binascii import hexlify
|
||||
|
||||
lastCMD = 0x00
|
||||
strFail = "Decode Fail"
|
||||
StatusCode = {
|
||||
0x00 : "OPERATION_OK",
|
||||
0x0C : "NO_CHANGES",
|
||||
0x0E : "ERR_OUT_OF_EEPROM",
|
||||
0x1C : "ILLEGAL_CMD_CODE",
|
||||
0x1E : "ERR_INTEGRITY",
|
||||
0x40 : "NO_SUCH_KEY",
|
||||
0x7E : "ERR_LENGTH",
|
||||
0x9D : "PERMISSION_DENIED",
|
||||
0x9E : "ERR_PARAMETER",
|
||||
0xA0 : "APP_NOT_FOUND",
|
||||
0xA1 : "ERR_APP_INTEGRITY",
|
||||
0xAE : "ERR_AUTH",
|
||||
0xAF : "ADDITIONAL_FRAME",
|
||||
0xBE : "ERR_BOUNDARY",
|
||||
0xC1 : "ERR_PICC_INTEGRITY",
|
||||
0xCA : "CMD_ABORTED",
|
||||
0xCD : "ERR_PICC_DISABLED",
|
||||
0xCE : "ERR_COUNT",
|
||||
0xDE : "ERR_DUPLICATE",
|
||||
0xEE : "ERR_EEPROM",
|
||||
0xF0 : "FILE_NOT_FOUND",
|
||||
0xF1 : "ERR_FILE_INTEGRITY"
|
||||
}
|
||||
#########################
|
||||
# PICC Level Commands
|
||||
#########################
|
||||
# Create APP CMD
|
||||
def decodeCreateAPP(data):
|
||||
note = ""
|
||||
if len(data) == 6:
|
||||
note += "AID:0x"+hexlify(data[1:4]).decode() + " "
|
||||
note += "KeySett:"+hex(data[4]) + " "
|
||||
note += "NumOfKeys:"+hex(data[5]) + " "
|
||||
else:
|
||||
note += strFail
|
||||
return note
|
||||
|
||||
def decodeDelAPP(data):
|
||||
if len(data) == 4:
|
||||
return "AID:0x"+hexlify(data[1:]).decode()
|
||||
else:
|
||||
return strFail
|
||||
|
||||
def decodeSelectAPP(data):
|
||||
if len(data) == 4:
|
||||
return "AID:0x"+ hexlify(data[1:4]).decode()
|
||||
else:
|
||||
return strFail
|
||||
|
||||
# Get APP ID CMD
|
||||
def decodeGetAPPID(data):
|
||||
if len(data) == 1:
|
||||
return ""
|
||||
else:
|
||||
return strFail
|
||||
|
||||
def decodeRespGetAPPID (data):
|
||||
note = "APPIDs: |"
|
||||
dataLen = len(data)
|
||||
|
||||
for i in range (0,int((dataLen-1)/3)):
|
||||
note += "0x"+hexlify(data[1+3*i: 1+3*(i+1)]).decode()+"|"
|
||||
|
||||
return note
|
||||
##############################
|
||||
# Security Related Commands
|
||||
##############################
|
||||
# Authenticate AES CMD
|
||||
def decodeAuthAES(data):
|
||||
if len(data) == 2:
|
||||
return "KeyNo:"+hex(data[1])
|
||||
else:
|
||||
return strFail
|
||||
|
||||
def decodeRespAuthAES(data):
|
||||
if len(data) == 17:
|
||||
return "ekNo(RndB):0x" + hexlify(data[1:]).decode()
|
||||
else:
|
||||
return strFail
|
||||
|
||||
def decodeAuthAESAF (data):
|
||||
if len(data) == 33:
|
||||
return "ekNo(RndA+RndB'):0x" + hexlify(data[1:]).decode()
|
||||
else:
|
||||
return strFail
|
||||
|
||||
def decodeCardAuthAESAF (data):
|
||||
if len(data) == 17:
|
||||
return "ekNo(RndA'):0x" + hexlify(data[1:]).decode()
|
||||
else:
|
||||
return strFail
|
||||
|
||||
# Authenticate 3DES CMD
|
||||
def decodeAuth3DES(data):
|
||||
if len(data) == 2:
|
||||
return "KeyNo:"+hex(data[1])
|
||||
else:
|
||||
return strFail
|
||||
|
||||
def decodeRespAuth3DES(data):
|
||||
if len(data) == 9:
|
||||
return "ekNo(RndB):0x" + hexlify(data[1:]).decode()
|
||||
else:
|
||||
return strFail
|
||||
|
||||
def decodeAuth3DESAF(data):
|
||||
if len(data) == 17:
|
||||
return "dkNo(RndA+RndB'):0x" + hexlify(data[1:]).decode()
|
||||
else:
|
||||
return strFail
|
||||
|
||||
def decodeCardAuth3DESAF (data):
|
||||
if len(data) == 9:
|
||||
return "ekNo(RndA'):0x" + hexlify(data[1:]).decode()
|
||||
else:
|
||||
return strFail
|
||||
|
||||
# ChangeKeySettings CMD
|
||||
def decodeChangeKeySettings(data):
|
||||
if len(data) == 9:
|
||||
return "KeySettings:0x" + hexlify(data[1:]).decode()
|
||||
else:
|
||||
return strFail
|
||||
# GetKeySettings CMD
|
||||
def decodeRespGetKeySettings(data):
|
||||
if len(data) == 3:
|
||||
return "KeySettings:" + hex(data[1]) + " MaxNoKeys:" + hex(data[2])
|
||||
else:
|
||||
return strFail
|
||||
|
||||
# ChangeKey CMD
|
||||
def decodeChangeKey(data):
|
||||
if len(data) == 26:
|
||||
return "KeyNo:" + hex(data[1]) + " decipheredKeyData:0x"+hexlify(data[2:]).decode()
|
||||
else:
|
||||
return strFail
|
||||
|
||||
# GetKeyVersion CMD
|
||||
def decodeGetKeyVersion(data):
|
||||
if len(data) == 2:
|
||||
return "KeyNo:" + hex(data[1])
|
||||
else:
|
||||
return strFail
|
||||
def decodeRespGetKeyVersion(data):
|
||||
return "KeyVersion" + hex(data[1]) if len(data) == 2 else strFail
|
||||
|
||||
##############################
|
||||
# Data Manipulation Commands
|
||||
##############################
|
||||
# Read Data
|
||||
def decodeFileNoOffsetLen(data):
|
||||
if len(data) == 8:
|
||||
fileNo = data[1]
|
||||
offSet = data[2:5]
|
||||
length = data[5:8]
|
||||
return "FileNo:"+hex(fileNo) + " OffSet:0x"+hexlify(offSet).decode() + " len:0x"+hexlify(length).decode()
|
||||
else:
|
||||
return strFail
|
||||
# Write Data
|
||||
def decodeFileNoOffsetLenData(data):
|
||||
if len(data) > 8:
|
||||
fileNo = data[1]
|
||||
offSet = data[2:5]
|
||||
length = data[5:8]
|
||||
dataWrite = data[8:]
|
||||
return "FileNo:"+hex(fileNo) + " OffSet:0x"+hexlify(offSet).decode() + " len:0x"+hexlify(length).decode() + \
|
||||
" Data:0x" + hexlify(dataWrite).decode()
|
||||
else:
|
||||
return strFail
|
||||
|
||||
# Get Value
|
||||
def decodeFileNo(data):
|
||||
if len(data) == 2:
|
||||
return "FileNo:"+hex(data[1])
|
||||
else:
|
||||
return strFail
|
||||
# Credit
|
||||
def decodeFileNoData(data):
|
||||
if len(data) >2:
|
||||
return "FileNo:" + hex(data[1]) + " Data:0x" + hexlify(data[2:]).decode()
|
||||
else:
|
||||
return strFail
|
||||
|
||||
def decodeData(data):
|
||||
return "Data:0x"+hexlify(data[1:]).decode() if len(data) >1 else strFail
|
||||
|
||||
###########################
|
||||
# Application Level Commands
|
||||
###########################
|
||||
def decodeRespGetFileIDs(data):
|
||||
if len(data) > 0:
|
||||
return "FIDs:0x"+hexlify(data[1:]).decode()
|
||||
else:
|
||||
return strFail
|
||||
|
||||
def decodeRespGetFileSettings(data):
|
||||
lenData = len(data)
|
||||
note = ""
|
||||
if lenData >5:
|
||||
note += "FileType:"+hex(data[1])+" ComSett:"+hex(data[2]) + "AccessRight:0x"+hexlify(data[3:5]).decode() + " "
|
||||
if lenData == 8:
|
||||
note += "FileSize:0x"+hexlify(data[5:]).decode()
|
||||
elif lenData == 18:
|
||||
note += "LowLimit:0x"+hexlify(data[5:9]).decode() + " "
|
||||
note += "UpLimit:0x" + hexlify(data[9:13]).decode() + " "
|
||||
note += "LimitedCreditVal:0x"+hexlify(data[13:17]).decode() + " "
|
||||
note += "LimitedCreditEn:" + hex(data[17])
|
||||
elif lenData == 14:
|
||||
note += "RecordSize:0x"+ hexlify(data[5:8]).decode() + " "
|
||||
note += "MaxNumRecords:0x"+hexlify(data[8:11]).decode() + " "
|
||||
note += "CurrentNumRecords:0x"+hexlify(data[11:]).decode()
|
||||
return note
|
||||
else:
|
||||
return strFail
|
||||
|
||||
def decodeFNComSetAccessRightsFileSize(data):
|
||||
if len(data) == 8:
|
||||
note = "FileNo:" + hex(data[1]) + " ComSet:" + hex(data[2]) + "AccessRight:0x" + hexlify(
|
||||
data[3:5]).decode() + " "
|
||||
note += "FileSize:0x" + hexlify(data[5:]).decode()
|
||||
return note
|
||||
else:
|
||||
return strFail
|
||||
|
||||
def decodeChangeFileSettings(data):
|
||||
lenData = len(data)
|
||||
note =""
|
||||
if lenData > 2:
|
||||
note += "FileNo:"+hex(data[1]) + " "
|
||||
if lenData == 5:
|
||||
note += "ComSet:"+hex(data[2]) + " "
|
||||
note += "AccessRights:0x"+hexlify(data[3:]).decode()
|
||||
elif lenData == 10:
|
||||
note += "NewSettings:0x"+hexlify(data[2:]).decode()
|
||||
return note
|
||||
else:
|
||||
return strFail
|
||||
|
||||
def decodeCreateValueFile(data):
|
||||
if len(data) == 18:
|
||||
note = "FileNo:"+hex(data[1])+" ComSet:"+hex(data[2]) + "AccessRight:0x"+hexlify(data[3:5]).decode() + " "
|
||||
note += "LowLimit:0x"+hexlify(data[5:9]).decode() + " "
|
||||
note += "UpLimit:0x" + hexlify(data[9:13]).decode() + " "
|
||||
note += "Val:0x"+hexlify(data[13:17]).decode() + " "
|
||||
note += "LimitedCreditEn:" + hex(data[17])
|
||||
return note
|
||||
else:
|
||||
return strFail
|
||||
|
||||
def decodeCreateRecordFile(data):
|
||||
if len(data) == 11:
|
||||
note = "FileNo:"+hex(data[1])+" ComSet:"+hex(data[2]) + "AccessRight:0x"+hexlify(data[3:5]).decode() + " "
|
||||
note += "RecordSize:0x"+hexlify(data[5:8]).decode()+" "
|
||||
note += "MaxNumRecords:0x"+hexlify(data[8:]).decode()
|
||||
return note
|
||||
else:
|
||||
return strFail
|
||||
|
||||
def decodeDelFile(data):
|
||||
if len(data) == 2:
|
||||
return "FileNo:"+hex(data[1])
|
||||
else:
|
||||
return strFail
|
||||
|
||||
def decodeAdiFrame(data):
|
||||
return "Data:0x"+hexlify(data[1:]).decode() if len(data) >1 else strFail
|
||||
|
||||
def decodeRespAdiFrame(data):
|
||||
return "Data:0x"+hexlify(data[1:]).decode() if len(data) >1 else strFail
|
||||
|
||||
def decodeDummy(data):
|
||||
return ""
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
MFDESFireCMDTypes = {
|
||||
# Application Level Commands
|
||||
0x6F : {"name": "GetFileIDs", "CMDdecoder":decodeDummy, "RespDecoder": decodeRespGetFileIDs},
|
||||
0xF5 : {"name": "GetFileSettings", "CMDdecoder":decodeFileNo, "RespDecoder": decodeRespGetFileSettings},
|
||||
0x5F : {"name": "ChangeFileSettings", "CMDdecoder":decodeChangeFileSettings, "RespDecoder": decodeDummy},
|
||||
0xCD : {"name": "CreateStdDataFile", "CMDdecoder":decodeFNComSetAccessRightsFileSize,"RespDecoder": decodeDummy},
|
||||
0xCB : {"name": "CreateBackupDataFile", "CMDdecoder":decodeFNComSetAccessRightsFileSize,"RespDecoder": decodeDummy},
|
||||
0xCC : {"name": "CreateValueFile", "CMDdecoder":decodeCreateValueFile, "RespDecoder": decodeDummy},
|
||||
0xC1 : {"name": "CreateLinearRecordFile","CMDdecoder":decodeCreateRecordFile, "RespDecoder": decodeDummy},
|
||||
0xC0 : {"name": "CreateCyclicRecordFile","CMDdecoder":decodeCreateRecordFile, "RespDecoder": decodeDummy},
|
||||
0xDF: {"name": "DelFile", "CMDdecoder":decodeDelFile, "RespDecoder": decodeDummy},
|
||||
|
||||
# PICC Level Commands, GetVersion not decoded, please refer to datasheet for the meaning of resp
|
||||
0xCA : {"name": "CreateApp ", "CMDdecoder":decodeCreateAPP, "RespDecoder": decodeDummy},
|
||||
0xDA : {"name": "DelApp ", "CMDdecoder":decodeDelAPP, "RespDecoder": decodeDummy},
|
||||
0x5A : {"name": "SelectApp ", "CMDdecoder":decodeSelectAPP, "RespDecoder": decodeDummy},
|
||||
0x6A : {"name": "GetAPPID ", "CMDdecoder":decodeGetAPPID, "RespDecoder": decodeRespGetAPPID},
|
||||
0xFC : {"name": "FormatPICC ", "CMDdecoder":decodeDummy, "RespDecoder": decodeDummy},
|
||||
0x60 : {"name": "GetVersion ", "CMDdecoder":decodeDummy, "RespDecoder": decodeDummy},
|
||||
# Data Manipulation Commands
|
||||
0xBD : {"name": "ReadData ", "CMDdecoder":decodeFileNoOffsetLen, "RespDecoder": decodeData},
|
||||
0x3D : {"name": "WriteData ", "CMDdecoder":decodeFileNoOffsetLenData, "RespDecoder": decodeDummy},
|
||||
0x6C : {"name": "GetValue ", "CMDdecoder":decodeFileNo, "RespDecoder": decodeData},
|
||||
0x0C : {"name": "Credit ", "CMDdecoder":decodeFileNoData, "RespDecoder": decodeDummy},
|
||||
0xDC : {"name": "Debit ", "CMDdecoder":decodeFileNoData, "RespDecoder": decodeDummy},
|
||||
0x1C : {"name": "LimitedCredit ", "CMDdecoder":decodeFileNoData, "RespDecoder": decodeDummy},
|
||||
0x3B : {"name": "WriteRecord ", "CMDdecoder":decodeFileNoOffsetLenData, "RespDecoder": decodeDummy},
|
||||
0xBB : {"name": "ReadRecord ", "CMDdecoder":decodeFileNoOffsetLen, "RespDecoder": decodeData},
|
||||
0xEB : {"name": "ClearRecordFile ", "CMDdecoder":decodeFileNo, "RespDecoder": decodeDummy},
|
||||
0xC7 : {"name": "CommitTransaction ","CMDdecoder":decodeDummy, "RespDecoder": decodeDummy},
|
||||
0xA7 : {"name": "AbortTransaction ","CMDdecoder":decodeDummy, "RespDecoder": decodeDummy},
|
||||
# Security Related CMD
|
||||
0xAA : {"name": "AuthAES ", "CMDdecoder": decodeAuthAES, "RespDecoder": decodeRespAuthAES},
|
||||
0x0A : {"name": "Auth3DES ", "CMDdecoder": decodeAuth3DES, "RespDecoder": decodeRespAuth3DES},
|
||||
0x54 : {"name": "ChangeKeySettings ","CMDdecoder":decodeChangeKeySettings,"RespDecoder": decodeDummy},
|
||||
0x45 : {"name": "GetKeySettings ", "CMDdecoder":decodeDummy, "RespDecoder": decodeRespGetKeySettings},
|
||||
0xC4 : {"name": "ChangeKey ", "CMDdecoder":decodeChangeKey, "RespDecoder": decodeDummy},
|
||||
0x64 : {"name": "GetKeyVersion ", "CMDdecoder":decodeGetKeyVersion, "RespDecoder": decodeRespGetKeyVersion},
|
||||
|
||||
# Additional Frame
|
||||
0xAF: {"name": "AdditionalFrame ", "CMDdecoder": decodeAdiFrame, "RespDecoder": decodeRespAdiFrame},
|
||||
|
||||
}
|
||||
|
||||
# Commands need to use additional frame
|
||||
MFDESFireAFCMD = {
|
||||
0xBD : {"name": "ReadData ", "AFReaderDecoder": decodeDummy, "AFCardDecoder": decodeData},
|
||||
0xAA : {"name": "AuthAES ", "AFReaderDecoder":decodeAuthAESAF, "AFCardDecoder": decodeCardAuthAESAF},
|
||||
0x0A : {"name": "Auth3DES ", "AFReaderDecoder":decodeAuth3DESAF, "AFCardDecoder": decodeCardAuth3DESAF},
|
||||
|
||||
0x3D : {"name": "WriteData ", "AFReaderDecoder":decodeData, "AFCardDecoder": decodeDummy},
|
||||
0xBB : {"name": "ReadRecord ", "AFReaderDecoder":decodeDummy, "AFCardDecoder": decodeData},
|
||||
|
||||
0x3B: {"name": "WriteRecord ", "AFReaderDecoder": decodeData, "AFCardDecoder": decodeDummy}
|
||||
|
||||
}
|
||||
|
||||
def MFDESFireDecode(data, source):
|
||||
note = ""
|
||||
global lastCMD
|
||||
if (source == TrafficSource.Reader):
|
||||
cmd = data[0]
|
||||
if cmd in MFDESFireCMDTypes :
|
||||
# If current frame is Additional Frame
|
||||
# And the previous cmd need Additional Frame
|
||||
if lastCMD in MFDESFireAFCMD and cmd == 0xAF:
|
||||
note += "CMD:AdditionalFrame "
|
||||
note += MFDESFireAFCMD[lastCMD]["AFReaderDecoder"](data)
|
||||
# Current command not need Additional Frame
|
||||
# or last cmd doesn't need AF
|
||||
else:
|
||||
lastCMD = cmd
|
||||
note += "CMD:" + MFDESFireCMDTypes[cmd]["name"]
|
||||
note += MFDESFireCMDTypes[cmd]["CMDdecoder"](data)
|
||||
|
||||
elif (source == TrafficSource.Card):
|
||||
status = data[0]
|
||||
# Decode status code
|
||||
if status in StatusCode:
|
||||
note += StatusCode[status] + " "
|
||||
# If status Ok, decode data
|
||||
if (status == 0x00 or status == 0xAF) and lastCMD in MFDESFireCMDTypes:
|
||||
# If last cmd need additional frame and this is the last frame
|
||||
if status == 0x00 and lastCMD in MFDESFireAFCMD:
|
||||
note += MFDESFireAFCMD[lastCMD]["AFCardDecoder"](data)
|
||||
else:
|
||||
note += MFDESFireCMDTypes[lastCMD]["RespDecoder"](data)
|
||||
|
||||
return note
|
||||
@@ -0,0 +1,5 @@
|
||||
from enum import Enum
|
||||
|
||||
class TrafficSource(Enum):
|
||||
Reader = 0
|
||||
Card = 1
|
||||
+9
-4
@@ -11,6 +11,7 @@ import json
|
||||
import Chameleon
|
||||
import io
|
||||
import datetime
|
||||
from Chameleon.ISO14443 import CardTypesMap
|
||||
|
||||
def verboseLog(text):
|
||||
formatString = "[{}] {}"
|
||||
@@ -19,9 +20,11 @@ def verboseLog(text):
|
||||
|
||||
def formatText(log):
|
||||
formatString = '{timestamp:0>5d} ms <{deltaTimestamp:>+6d} ms>:'
|
||||
formatString += '{eventName:<28} ({dataLength:<3} bytes)\t[{data}]\n'
|
||||
formatString += '{eventName:<28} ({dataLength:<3} bytes) [{data:<20}] ' \
|
||||
'\033[94m {note} \x1b[0m \n'
|
||||
|
||||
text = ''
|
||||
|
||||
|
||||
for logEntry in log:
|
||||
text += formatString.format(**logEntry)
|
||||
|
||||
@@ -46,6 +49,7 @@ def main():
|
||||
|
||||
argParser.add_argument("-t", "--type", choices=outputTypes.keys(), default='text',
|
||||
help="specifies output type")
|
||||
argParser.add_argument("-d", "--decode", dest="decode", choices=CardTypesMap.keys(), default=None, help="Decode the sniffed traffic and application data with a decoder")
|
||||
argParser.add_argument("-l", "--live", dest="live", action='store_true', help="Use live logging capabilities of Chameleon")
|
||||
argParser.add_argument("-c", "--clear", dest="clear", action='store_true', help="Clear Chameleon's log memory when using -p")
|
||||
argParser.add_argument("-m", "--mode", dest="mode", metavar="LOGMODE", help="Additionally set Chameleon's log mode after reading it's memory")
|
||||
@@ -58,6 +62,7 @@ def main():
|
||||
else:
|
||||
verboseFunc = None
|
||||
|
||||
print("\nNote: If parityBit check failed, '!' is appended to the decoded data and raw data with parity bit is displayed.\n")
|
||||
if (args.live):
|
||||
# Live logging mode
|
||||
if (args.port is not None):
|
||||
@@ -68,7 +73,7 @@ def main():
|
||||
|
||||
while True:
|
||||
stream = io.BytesIO(chameleon.read())
|
||||
log = Chameleon.Log.parseBinary(stream)
|
||||
log = Chameleon.Log.parseBinary(stream, args.decode)
|
||||
if (len(log) > 0):
|
||||
print(outputTypes[args.type](log))
|
||||
|
||||
@@ -94,7 +99,7 @@ def main():
|
||||
sys.exit(2)
|
||||
|
||||
# Parse actual logfile
|
||||
log = Chameleon.Log.parseBinary(handle)
|
||||
log = Chameleon.Log.parseBinary(handle, args.decode)
|
||||
|
||||
# Print to console using chosen output type
|
||||
print(outputTypes[args.type](log))
|
||||
|
||||
+35
-1
@@ -68,6 +68,17 @@ def cmdLog(chameleon, arg):
|
||||
bytesReceived = chameleon.cmdDownloadLog(fileHandle)
|
||||
return "{} Bytes successfully written to {}".format(bytesReceived, arg)
|
||||
|
||||
def cmdLogMode(chameleon, arg):
|
||||
result = chameleon.cmdLogMode(arg)
|
||||
|
||||
if (arg is None):
|
||||
return "Current logmode is: {}".format(result['response'])
|
||||
else:
|
||||
if (result['statusCode'] in chameleon.STATUS_CODES_SUCCESS):
|
||||
return "logmode have been set to {}".format(arg)
|
||||
else:
|
||||
return "Setting logmode failed: {}".format(arg, result['statusText'])
|
||||
|
||||
def cmdLButton(chameleon, arg):
|
||||
result = chameleon.cmdLButton(arg)
|
||||
|
||||
@@ -119,7 +130,23 @@ def cmdRedLED(chameleon, arg):
|
||||
return "Red LED function has been set to {}".format(chameleon.cmdRedLED()['response'])
|
||||
else:
|
||||
return "Setting red LED function to {} failed: {}".format(arg, result['statusText'])
|
||||
|
||||
|
||||
def cmdThreshold(chameleon, arg):
|
||||
result = chameleon.cmdThreshold(arg)
|
||||
|
||||
if (arg is None):
|
||||
return "Current threshold is: {}".format(result['response'])
|
||||
else:
|
||||
if (result['statusCode'] in chameleon.STATUS_CODES_SUCCESS):
|
||||
return "Threshold have been set to {}".format(arg)
|
||||
else:
|
||||
return "Setting threshold failed: {}".format(arg, result['statusText'])
|
||||
|
||||
def cmdUpgrade(chameleon, arg):
|
||||
if(chameleon.cmdUpgrade() == 0):
|
||||
print ("Device changed into Upgrade Mode")
|
||||
exit(0)
|
||||
|
||||
# Custom class for argparse
|
||||
class CmdListAction(argparse.Action):
|
||||
def __init__(self, option_strings, dest, default=False, required=False,
|
||||
@@ -150,10 +177,14 @@ def main():
|
||||
cmdArgGroup.add_argument("-s", "--setting", dest="setting", action=CmdListAction, nargs='?', type=int, choices=Chameleon.VALID_SETTINGS, help="retrieve or set the current setting")
|
||||
cmdArgGroup.add_argument("-U", "--uid", dest="uid", action=CmdListAction, nargs='?', help="retrieve or set the current UID")
|
||||
cmdArgGroup.add_argument("-c", "--config", dest="config", action=CmdListAction, metavar="CFGNAME", nargs='?', help="retrieve or set the current configuration")
|
||||
cmdArgGroup.add_argument("-lm", "--logmode", dest="logmode", action=CmdListAction, metavar="LOGMODE", nargs='?', help="retrieve or set the current log mode")
|
||||
cmdArgGroup.add_argument("-lb", "--lbutton", dest="lbutton", action=CmdListAction, metavar="ACTION", nargs='?', help="retrieve or set the current left button action")
|
||||
cmdArgGroup.add_argument("-rb", "--rbutton", dest="rbutton", action=CmdListAction, metavar="ACTION", nargs='?', help="retrieve or set the current right button action")
|
||||
cmdArgGroup.add_argument("-gl", "--gled", dest="gled", action=CmdListAction, metavar="FUNCTION", nargs='?', help="retrieve or set the current green led function")
|
||||
cmdArgGroup.add_argument("-rl", "--rled", dest="rled", action=CmdListAction, metavar="FUNCTION", nargs='?', help="retrieve or set the current red led function")
|
||||
cmdArgGroup.add_argument("-th", "--threshold", dest="threshold", action=CmdListAction, nargs='?', help="retrieve or set the threshold")
|
||||
cmdArgGroup.add_argument("-ug", "--upgrade", dest="upgrade", action=CmdListAction, nargs=0, help="set the micro Controller to upgrade mode")
|
||||
|
||||
args = argParser.parse_args()
|
||||
|
||||
if (args.verbose):
|
||||
@@ -175,10 +206,13 @@ def main():
|
||||
"upload" : cmdUpload,
|
||||
"download" : cmdDownload,
|
||||
"log" : cmdLog,
|
||||
"logmode" : cmdLogMode,
|
||||
"lbutton" : cmdLButton,
|
||||
"rbutton" : cmdRButton,
|
||||
"gled" : cmdGreenLED,
|
||||
"rled" : cmdRedLED,
|
||||
"threshold" : cmdThreshold,
|
||||
"upgrade" : cmdUpgrade,
|
||||
}
|
||||
|
||||
if hasattr(args, "cmdList"):
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
pyserial
|
||||
crcmod
|
||||
Reference in New Issue
Block a user