diff --git a/Software/Chameleon/Device.py b/Software/Chameleon/Device.py index 57169c7..bfe2267 100644 --- a/Software/Chameleon/Device.py +++ b/Software/Chameleon/Device.py @@ -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 diff --git a/Software/Chameleon/ISO14443.py b/Software/Chameleon/ISO14443.py new file mode 100644 index 0000000..55565e9 --- /dev/null +++ b/Software/Chameleon/ISO14443.py @@ -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) diff --git a/Software/Chameleon/Log.py b/Software/Chameleon/Log.py index 0614e1b..524ecab 100644 --- a/Software/Chameleon/Log.py +++ b/Software/Chameleon/Log.py @@ -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) diff --git a/Software/Chameleon/MFDESFire.py b/Software/Chameleon/MFDESFire.py new file mode 100644 index 0000000..07577ef --- /dev/null +++ b/Software/Chameleon/MFDESFire.py @@ -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 diff --git a/Software/Chameleon/utils.py b/Software/Chameleon/utils.py new file mode 100644 index 0000000..af4a5ab --- /dev/null +++ b/Software/Chameleon/utils.py @@ -0,0 +1,5 @@ +from enum import Enum + +class TrafficSource(Enum): + Reader = 0 + Card = 1 diff --git a/Software/chamlog.py b/Software/chamlog.py index 76dd87e..d12e3d3 100755 --- a/Software/chamlog.py +++ b/Software/chamlog.py @@ -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)) diff --git a/Software/chamtool.py b/Software/chamtool.py index 6f97a96..aff659c 100755 --- a/Software/chamtool.py +++ b/Software/chamtool.py @@ -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"): diff --git a/Software/requirements.txt b/Software/requirements.txt index f6c1a1f..5900a94 100644 --- a/Software/requirements.txt +++ b/Software/requirements.txt @@ -1 +1,2 @@ pyserial +crcmod \ No newline at end of file