diff options
Diffstat (limited to 'tools/smartcard-interpreter.py')
-rw-r--r-- | tools/smartcard-interpreter.py | 572 |
1 files changed, 572 insertions, 0 deletions
diff --git a/tools/smartcard-interpreter.py b/tools/smartcard-interpreter.py new file mode 100644 index 0000000..2fd1605 --- /dev/null +++ b/tools/smartcard-interpreter.py @@ -0,0 +1,572 @@ +#!/usr/bin/env python3 +# +# Copyright 2022 David Fort <contact@hardening-consulting.com> +# +# This script is meant to parse some FreeRDP logs in DEBUG mode (WLOG_LEVEL=DEBUG) and interpret the +# smartcard traffic, dissecting the PIV or GIDS commands +# +# usage: +# * live: WLOG_LEVEL=DEBUG xfreerdp <args with smartcard> | python3 smartcard-interpreter.py +# * on an existing log file: python3 smartcard-interpreter.py <log file> +# +import sys +import codecs + + +CMD_NAMES = { + 0x04: "DESACTIVATE FILE", + 0x0C: "ERASE RECORD", + 0x0E: "ERASE BINARY", + 0x0F: "ERASE BINARY", + 0x20: "VERIFY", + 0x21: "VERIFY", + 0x22: "MSE", + 0x24: "CHANGE REFERENCE DATA", + 0x25: "MSE", + 0x26: "DISABLE VERIFICATION REQUIREMENT", + 0x28: "ENABLE VERIFICATION REQUIREMENT", + 0x2A: "PSO", + 0x2C: "RESET RETRY COUNTER", + 0x2D: "RESET RETRY COUNTER", + 0x44: "ACTIVATE FILE", + 0x46: "GENERATE ASYMMETRIC KEY PAIR", + 0x47: "GENERATE ASYMMETRIC KEY PAIR", + 0x84: "GET CHALLENGE", + 0x86: "GENERAL AUTHENTICATE", + 0x87: "GENERAL AUTHENTICATE", + 0x88: "INTERNAL AUTHENTICATE", + 0xA0: "SEARCH BINARY", + 0xA1: "SEARCH BINARY", + 0xA2: "SEARCH RECORD", + 0xA4: "SELECT", + 0xB0: "READ BINARY", + 0xB1: "READ BINARY", + 0xB2: "READ RECORD", + 0xB3: "READ RECORD", + 0xC0: "GET RESPONSE", + 0xC2: "ENVELOPPE", + 0xC3: "ENVELOPPE", + 0xCA: "GET DATA", + 0xCB: "GET DATA", + 0xD0: "WRITE BINARY", + 0xD1: "WRITE BINARY", + 0xD2: "WRITE RECORD", + 0xD6: "UPDATE BINARY", + 0xD7: "UPDATE BINARY", + 0xDA: "PUT DATA", + 0xDB: "PUT DATA", + 0xDC: "UPDATE RECORD", + 0xDD: "UPDATE RECORD", + 0xE0: "CREATE FILE", + 0xE2: "APPEND RECORD", + 0xE4: "DELETE FILE", + 0xE6: "TERMINATE DF", + 0xE8: "TERMINATE EF", + 0xFE: "TERMINATE CARD USAGE", +} + +AIDs = { + "a00000039742544659": "MsGidsAID", + "a000000308": "PIV", + "a0000003974349445f0100": "SC PNP", +} + +FIDs = { + 0x0000: "Current EF", + 0x2F00: "EF.DIR", + 0x2F01: "EF.ATR", + 0x3FFF: "Current application(ADF)", +} + +DOs = { + "df1f": "DO_FILESYSTEMTABLE", + "df20": "DO_CARDID", + "df21": "DO_CARDAPPS", + "df22": "DO_CARDCF", + "df23": "DO_CMAPFILE", + "df24": "DO_KXC00", +} + +ERROR_CODES = { + 0x9000: "success", + 0x6282: "end of file or record", + 0x63C0: "warning counter 0", + 0x63C1: "warning counter 1", + 0x63C2: "warning counter 2", + 0x63C3: "warning counter 3", + 0x63C4: "warning counter 4", + 0x63C5: "warning counter 5", + 0x63C6: "warning counter 6", + 0x63C7: "warning counter 7", + 0x63C8: "warning counter 8", + 0x63C9: "warning counter 9", + 0x6982: "security status not satisfied", + 0x6985: "condition of use not satisfied", + 0x6A80: "incorrect parameter cmd data field", + 0x6A81: "function not suppported", + 0x6A82: "file or application not found", + 0x6A83: "record not found", + 0x6A88: "REFERENCE DATA NOT FOUND", + 0x6D00: "unsupported", +} + +PIV_OIDs = { + "5fc101": "X.509 Certificate for Card Authentication", + "5fc102": "Card Holder Unique Identifier", + "5fc103": "Cardholder Fingerprints", + "5fc105": "X.509 Certificate for PIV Authentication", + "5fc106": "Security Object", + "5fc107": "Card Capability Container", + "5fc108": "Cardholder Facial Image", + "5fc10a": "X.509 Certificate for Digital Signature", + "5fc10b": "X.509 Certificate for Key Management", + "5fc10d": "Retired X.509 Certificate for Key Management 1", + "5fc10e": "Retired X.509 Certificate for Key Management 2", + "5fc10f": "Retired X.509 Certificate for Key Management 3", +} + +class ApplicationDummy(object): + def __init__(self, aid): + self.aid = aid + + def getAID(self): + return self.aid + + def selectResult(self, fci, status, body): + return 'selectResult(%s, %s, %s)\n' %(fci, status, body.hex()) + + def getData(self, fileId, bytes): + return 'getData(0x%x, %s)\n' %(fileId, bytes.hex()) + + def getDataResult(self, status, body): + return 'getDataResult(0x%x, %s)\n' %(status, body.hex()) + + def mse(self, body): + return body.hex() + + def mseResult(self, status, body): + return body.hex() + + def pso(self, body): + return body.hex() + + def psoResult(self, status, body): + return body.hex() + + +class ApplicationPIV(object): + def __init__(self, aid): + self.lastGet = None + self.aid = aid + + def getAID(self): + return self.aid + + def selectResult(self, selectT, status, body): + ret = '' + appTag = body[0] + appLen = body[1] + + body = body[2:2+appLen] + while len(body) > 2: + tag = body[0] + tagLen = body[1] + if selectT == "FCI": + if tag == 0x4f: + ret += "\tpiv version: %s\n" % body[2:2 + tagLen].hex() + elif tag == 0x79: + subBody = body[2:2 + tagLen] + + subTag = subBody[0] + subLen = subBody[1] + + content = subBody.hex() + if subTag == 0x4f: + v = content[4:] + if v.startswith('a000000308'): + content = 'NIST RID' + ret += '\tCoexistent tag allocation authority: %s\n' % content + + elif tag == 0x50: + ret += '\tapplication label\n' + elif tag == 0xac: + ret += '\tCryptographic algorithms supported\n' + else: + rety += '\tunknown tag 0x%x\n' % tag + + else: + ret += "\tTODO: selectType %s\n" % selectT + + body = body[2+tagLen:] + + return ret + + def getData(self, fileId, bytes): + ret = "\tfileId=%s\n" % FIDs.get(fileId, "%0.4x" % fileId) + + lc = bytes[4] + tag = bytes[5] + tagLen = bytes[6] + + if lc == 4: + ret += "\tdoId=%0.4x\n"% (bytes[7] * 256 + bytes[8]) + + elif lc == 0xa: + keyStr = '' + # TLV + i = 7 + tag = bytes[i] + tagLen = bytes[i+1] + keyRef = bytes[i+3] + keyStr = "key(tag=0x%x len=%d ref=0x%x)=" % (tag, tagLen, keyRef) + i = i + 2 + tagLen + + tag = bytes[i] + tagLen = bytes[i+1] + keyStr += "value(tag=0x%x len=%d)" + elif lc == 5: + if tag == 0x5C: + tagStr = bytes[7:].hex() + ret += '\ttag: %s(%s)\n' % (tagStr, PIV_OIDs.get(tagStr, '<unknown>')) + self.lastGet = tagStr + else: + ret += "\tunknown key access\n" + + return ret + + def getDataResult(self, status, body): + ret = '' + if not len(body): + return '' + appTag = body[0] + appLen = body[1] + + body = body[2:2+appLen] + while len(body) > 2: + tag = body[0] + tagLen = body[1] + tagBody = body[2:2+tagLen] + + if self.lastGet in ('5fc102',): + # Card holder Unique Identifier + if tag == 0x30: + ret += '\tFASC-N: %s\n' % tagBody.hex() + elif tag == 0x34: + ret += '\tGUID: %s\n' % tagBody.hex() + elif tag == 0x35: + ret += '\texpirationDate: %s\n' % tagBody.decode('utf8') + elif tag == 0x3e: + ret += '\tIssuer Asymmetric Signature: %s\n' % tagBody.hex() + else: + ret += "\tunknown tag=0x%x len=%d content=%s\n" % (tag, tagLen, tagBody.hex()) + else: + ret += "\t%s: unknown tag=0x%x len=%d content=%s\n" % (self.lastGet, tag, tagLen, tagBody.hex()) + + body = body[2+tagLen:] + + return ret + + def mse(self, body): + return body.hex() + + def mseResult(self, status, body): + return body.hex() + + def pso(self, body): + return body.hex() + + def psoResult(self, status, body): + return body.hex() + + + +class ApplicationGids(object): + def __init__(self, aid): + self.aid = aid + self.lastDo = None + + def getAID(self): + return self.aid + + def parseFcp(self, bytes): + ret = '' + tag = bytes[0] + tagLen = bytes[1] + + body = bytes[2:2+tagLen] + + if tag == 0x62: + ret += '\tFCP\n' + + while len(body) > 2: + tag2 = body[0] + tag2Len = body[1] + tag2Body = body[2:2+tag2Len] + + if tag2 == 0x82: + ret += '\t\tFileDescriptor: %s\n' % tag2Body.hex() + elif tag2 == 0x8a: + ret += '\t\tLifeCycleByte: %s\n' % tag2Body.hex() + elif tag2 == 0x84: + ret += '\t\tDF name: %s\n' % tag2Body.encode('utf8') + elif tag2 == 0x8C: + ret += '\t\tSecurityAttributes: %s\n' % tag2Body.hex() + else: + ret += '\t\tunhandled tag=0x%x body=%s\n' % (tag2, tag2Body.hex()) + + body = body[2+tag2Len:] + + return ret + + def parseFci(self, bytes): + ret = '' + tag = bytes[0] + tagLen = bytes[1] + + body = bytes[2:2+tagLen] + + if tag == 0x61: + ret += '\tFCI\n' + + while len(body) > 2: + tag2 = body[0] + tag2Len = body[1] + tag2Body = body[2:2+tag2Len] + + if tag2 == 0x4F: + ret += '\t\tApplication AID: %s\n' % tag2Body.hex() + + elif tag2 == 0x50: + ret += '\t\tApplication label: %s\n' % tag2Body.encode('utf8') + + elif tag2 == 0x73: + body2 = tag2Body + tokens = [] + while len(body2) > 2: + tag3 = body2[0] + tag3Len = body2[1] + + if tag3 == 0x40: + v = body2[2] + if v & 0x80: + tokens.append('mutualAuthSymAlgo') + if v & 0x40: + tokens.append('extAuthSymAlgo') + if v & 0x20: + tokens.append('keyEstabIntAuthECC') + + + body2 = body2[2+tag3Len:] + + ret += '\t\tDiscretionary data objects: %s\n' % ",".join(tokens) + else: + ret += '\t\tunhandled tag=0x%x body=%s\n' % (tag2, tag2Body.hex()) + + body = body[2+tag2Len:] + + return ret + + + def selectResult(self, selectT, status, body): + if not len(body): + return '' + + if selectT == 'FCP': + return self.parseFcp(body) + elif selectT == 'FCI': + return self.parseFci(body) + else: + return '\tselectResult(%s, %s, %s)\n' % (selectT, status, body.hex()) + + def getData(self, fileId, bytes): + lc = bytes[4] + tag = bytes[5] + tagLen = bytes[6] + + if tag == 0x5c: + doStr = bytes[7:7+tagLen].hex() + ret = '\tDO=%s\n' % DOs.get(doStr, "<%s>" % doStr) + self.lastDo = doStr + else: + ret = '\tunknown tag=0%x len=%d v=%s' % (tag, tagLen, bytes[7:7+tagLen].hex()) + + return ret + + def getDataResult(self, status, body): + ret = '' + ''' + while len(body) > 2: + tag = body[0] + tagLen = body[1] + + ret += '\ttag=0x%x len=%d content=%s\n' % (tag, tagLen, body[2:2+tagLen].hex()) + + body = body[2+tagLen:] + ''' + return ret + + def mse(self, body): + return body.hex() + + def mseResult(self, status, body): + return body.hex() + + def pso(self, body): + return body.hex() + + def psoResult(self, status, body): + return body.hex() + + + +def createAppByAid(aid): + if aid == "a000000308": + return ApplicationPIV(aid) + + elif aid in ('a00000039742544659',): + return ApplicationGids(aid) + + return ApplicationDummy(aid) + + +if __name__ == '__main__': + if len(sys.argv) > 1: + fin = open(sys.argv[1], "r") + else: + fin = sys.stdin + + lineno = 0 + lastCmd = 0 + lastSelect = None + lastSelectFCI = False + lastGetItem = None + currentApp = None + + for l in fin.readlines(): + lineno += 1 + + if not len(l): + continue + + # smartcard loggers have changed + #if l.find("[DEBUG][com.freerdp.channels.smartcard.client]") == -1: + # continue + + body = '' + recvKey = 'pbRecvBuffer: { ' + + pos = l.find(recvKey) + if pos != -1: + toCard = False + + pos += len(recvKey) + pos2 = l.find(' }', pos) + if pos2 == -1: + print("line %d: invalid recvBuffer") + continue + + else: + toCard = True + sendKey = 'pbSendBuffer: { ' + pos = l.find(sendKey) + if pos == -1: + continue + pos += len(sendKey) + + pos2 = l.find(' }', pos) + if pos2 == -1: + print("line %d: invalid sendBuffer") + continue + + body = l[pos:pos2] + + print(l[0:-1]) + bytes = codecs.decode(body, 'hex') + if toCard: + (cla, ins, p1, p2) = bytes[0:4] + cmdName = CMD_NAMES.get(ins, "<COMMAND 0x%x>" % ins) + print(cmdName + ":") + + if cmdName == "SELECT": + lc = bytes[4] + i = 5 + + if p1 == 0x00: + print("\tselectByFID: %0.2x%0.2x" % (bytes[i], bytes[i+1])) + i = i + lc + + elif p1 == 0x4: + aid = bytes[i:i+lc].hex() + lastSelect = AIDs.get(aid, '') + print("\tselectByAID: %s(%s)" % (aid, lastSelect)) + + if p2 == 0x00: + lastSelectT = "FCI" + print('\tFCI') + elif p2 == 0x04: + print('\tFCP') + lastSelectT = "FCP" + elif p2 == 0x08: + print('\tFMD') + lastSelectT = "FMD" + + if not currentApp or currentApp.getAID() != aid: + currentApp = createAppByAid(aid) + + + elif cmdName == "VERIFY": + lc = bytes[4] + P2_DATA_QUALIFIER = { + 0x00: "Card global password", + 0x01: "RFU", + 0x80: "Application password", + 0x81: "Application resetting password", + 0x82: "Application security status resetting code", + } + + pin='' + if lc: + pin = ", pin='" + bytes[5:5+lc-2].decode('utf8)') + "'" + + print("\t%s%s" % (P2_DATA_QUALIFIER.get(p2, "<unknown>"), pin)) + + elif cmdName == "GET DATA": + lc = bytes[4] + fileId = p1 * 256 + p2 + + ret = currentApp.getData(fileId, bytes) + print("%s" % ret) + + elif cmdName == "MSE": + ret = currentApp.mse(bytes[5:5+lc]) + print("%s" % ret) + + elif cmdName == "PSO": + ret = currentApp.pso(bytes[5:5+lc]) + print("%s" % ret) + else: + print('handle %s' % cmdName) + + lastCmd = cmdName + + else: + # Responses + status = bytes[-1] + bytes[-2] * 256 + body = bytes[0:-2] + print("status=0x%0.4x(%s)" % (status, ERROR_CODES.get(status, "<unknown>"))) + + if not len(body): + continue + + ret = '' + if lastCmd == "SELECT": + ret = currentApp.selectResult(lastSelectT, status, body) + elif lastCmd == "GET DATA": + ret = currentApp.getDataResult(status, body) + elif lastCmd == "MSE": + ret = currentApp.mseResult(status, body) + elif lastCmd == "PSO": + ret = currentApp.psoResult(status, body) + + if ret: + print("%s" % ret)
\ No newline at end of file |