FreeRDP
smartcard-interpreter.py
1 #!/usr/bin/env python3
2 #
3 # Copyright 2022 David Fort <contact@hardening-consulting.com>
4 #
5 # This script is meant to parse some FreeRDP logs in DEBUG mode (WLOG_LEVEL=DEBUG) and interpret the
6 # smartcard traffic, dissecting the PIV or GIDS commands
7 #
8 # usage:
9 # * live: WLOG_LEVEL=DEBUG xfreerdp <args with smartcard> | python3 smartcard-interpreter.py
10 # * on an existing log file: python3 smartcard-interpreter.py <log file>
11 #
12 import sys
13 import codecs
14 
15 
16 CMD_NAMES = {
17  0x04: "DESACTIVATE FILE",
18  0x0C: "ERASE RECORD",
19  0x0E: "ERASE BINARY",
20  0x0F: "ERASE BINARY",
21  0x20: "VERIFY",
22  0x21: "VERIFY",
23  0x22: "MSE",
24  0x24: "CHANGE REFERENCE DATA",
25  0x25: "MSE",
26  0x26: "DISABLE VERIFICATION REQUIREMENT",
27  0x28: "ENABLE VERIFICATION REQUIREMENT",
28  0x2A: "PSO",
29  0x2C: "RESET RETRY COUNTER",
30  0x2D: "RESET RETRY COUNTER",
31  0x44: "ACTIVATE FILE",
32  0x46: "GENERATE ASYMMETRIC KEY PAIR",
33  0x47: "GENERATE ASYMMETRIC KEY PAIR",
34  0x84: "GET CHALLENGE",
35  0x86: "GENERAL AUTHENTICATE",
36  0x87: "GENERAL AUTHENTICATE",
37  0x88: "INTERNAL AUTHENTICATE",
38  0xA0: "SEARCH BINARY",
39  0xA1: "SEARCH BINARY",
40  0xA2: "SEARCH RECORD",
41  0xA4: "SELECT",
42  0xB0: "READ BINARY",
43  0xB1: "READ BINARY",
44  0xB2: "READ RECORD",
45  0xB3: "READ RECORD",
46  0xC0: "GET RESPONSE",
47  0xC2: "ENVELOPPE",
48  0xC3: "ENVELOPPE",
49  0xCA: "GET DATA",
50  0xCB: "GET DATA",
51  0xD0: "WRITE BINARY",
52  0xD1: "WRITE BINARY",
53  0xD2: "WRITE RECORD",
54  0xD6: "UPDATE BINARY",
55  0xD7: "UPDATE BINARY",
56  0xDA: "PUT DATA",
57  0xDB: "PUT DATA",
58  0xDC: "UPDATE RECORD",
59  0xDD: "UPDATE RECORD",
60  0xE0: "CREATE FILE",
61  0xE2: "APPEND RECORD",
62  0xE4: "DELETE FILE",
63  0xE6: "TERMINATE DF",
64  0xE8: "TERMINATE EF",
65  0xFE: "TERMINATE CARD USAGE",
66 }
67 
68 AIDs = {
69  "a00000039742544659": "MsGidsAID",
70  "a000000308": "PIV",
71  "a0000003974349445f0100": "SC PNP",
72  "a0000001510000": "GPC",
73 }
74 
75 FIDs = {
76  0x0000: "Current EF",
77  0x2F00: "EF.DIR",
78  0x2F01: "EF.ATR",
79  0x3FFF: "Current application(ADF)",
80 }
81 
82 DOs = {
83  "df1f": "DO_FILESYSTEMTABLE",
84  "df20": "DO_CARDID",
85  "df21": "DO_CARDAPPS",
86  "df22": "DO_CARDCF",
87  "df23": "DO_CMAPFILE",
88  "df24": "DO_KXC00",
89 }
90 
91 ERROR_CODES = {
92  0x9000: "success",
93  0x6100: "more bytes",
94  0x6282: "end of file or record",
95  0x6283: "card locked",
96  0x63C0: "warning counter 0",
97  0x63C1: "warning counter 1",
98  0x63C2: "warning counter 2",
99  0x63C3: "warning counter 3",
100  0x63C4: "warning counter 4",
101  0x63C5: "warning counter 5",
102  0x63C6: "warning counter 6",
103  0x63C7: "warning counter 7",
104  0x63C8: "warning counter 8",
105  0x63C9: "warning counter 9",
106  0x6982: "security status not satisfied",
107  0x6882: "Secure messaging not supported",
108  0x6985: "condition of use not satisfied",
109  0x6A80: "incorrect parameter cmd data field",
110  0x6A81: "function not suppported",
111  0x6A82: "file or application not found",
112  0x6A83: "record not found",
113  0x6A88: "REFERENCE DATA NOT FOUND",
114  0x6D00: "unsupported",
115 }
116 
117 PIV_OIDs = {
118  "5fc101": "X.509 Certificate for Card Authentication",
119  "5fc102": "Card Holder Unique Identifier",
120  "5fc103": "Cardholder Fingerprints",
121  "5fc105": "X.509 Certificate for PIV Authentication",
122  "5fc106": "Security Object",
123  "5fc107": "Card Capability Container",
124  "5fc108": "Cardholder Facial Image",
125  "5fc10a": "X.509 Certificate for Digital Signature",
126  "5fc10b": "X.509 Certificate for Key Management",
127  "5fc10d": "Retired X.509 Certificate for Key Management 1",
128  "5fc10e": "Retired X.509 Certificate for Key Management 2",
129  "5fc10f": "Retired X.509 Certificate for Key Management 3",
130 }
131 
132 class ApplicationDummy(object):
133  """Base application"""
134 
135  def __init__(self, aid):
136  self.aidaid = aid
137 
138  def getAID(self):
139  return self.aidaid
140 
141  def selectResult(self, fci, status, body):
142  return 'selectResult(fci=%s, status=0x%x) = %s\n' %(fci, status, body.hex())
143 
144  def getData(self, fileId, bytes):
145  return 'getData(status=0x%x) = %s\n' %(fileId, bytes.hex())
146 
147  def getDataResult(self, status, body):
148  return 'getDataResult(status=0x%x) = %s\n' %(status, body.hex())
149 
150  def mse(self, body):
151  return body.hex()
152 
153  def mseResult(self, status, body):
154  return body.hex()
155 
156  def pso(self, body):
157  return body.hex()
158 
159  def psoResult(self, status, body):
160  return body.hex()
161 
162  def getResponse(self, status, body):
163  return body.hex()
164 
165  def getResponseResult(self, status, body):
166  return body.hex()
167 
168  def verify(self, status, body):
169  return "verify(%s)" % body.hex()
170 
171  def verifyResult(self, status, body):
172  return "verify(%s)" % body.hex()
173 
174 
175 '''
176 '''
177 class ApplicationGpc(object):
178  """GlobalPlatform application"""
179 
180  def __init__(self, aid):
181  self.aidaid = aid
182  self.lastGetDatalastGetData = None
183 
184  def getAID(self):
185  return self.aidaid
186 
187  def selectResult(self, fci, status, body):
188  return 'selectResult(fci=%s, status=0x%x) = %s\n' %(fci, status, body.hex())
189 
190  def getData(self, fileId, bytes):
191  tags = {
192  0x42: 'Issuer Identification Number',
193  0x45: 'Card Image Number',
194  0x66: 'Card Data',
195  0x67: 'Card Capability Information' # ???
196  }
197  self.lastGetDatalastGetData = fileId
198  return 'getData(%s)\n' % tags.get(fileId, '<unknown 0x%x>' % fileId)
199 
200  def getDataResult(self, status, body):
201  if self.lastGetDatalastGetData == 0x66:
202  # Card Data
203  pass
204  return 'getDataResult(0x%x) = %s\n' %(status, body.hex())
205 
206  def mse(self, body):
207  return body.hex()
208 
209  def mseResult(self, status, body):
210  return body.hex()
211 
212  def pso(self, body):
213  return body.hex()
214 
215  def psoResult(self, status, body):
216  return body.hex()
217 
218  def getResponse(self, status, body):
219  return body.hex()
220 
221  def getResponseResult(self, status, body):
222  return body.hex()
223 
224 
225 class ApplicationPIV(object):
226  """PIV application"""
227 
228  def __init__(self, aid):
229  self.lastGetlastGet = None
230  self.aidaid = aid
231 
232  def getAID(self):
233  return self.aidaid
234 
235  def selectResult(self, selectT, status, body):
236  ret = ''
237  appTag = body[0]
238  appLen = body[1]
239 
240  body = body[2:2+appLen]
241  while len(body) > 2:
242  tag = body[0]
243  tagLen = body[1]
244  if tagLen != 1:
245  pass
246  if selectT == "FCI":
247  if tag == 0x4f:
248  ret += "\tpiv version: %s\n" % body[2:2 + tagLen].hex()
249  elif tag == 0x79:
250  subBody = body[2:2 + tagLen]
251 
252  subTag = subBody[0]
253  subLen = subBody[1]
254 
255  content = subBody.hex()
256  if subTag == 0x4f:
257  v = content[4:]
258  if v.startswith('a000000308'):
259  content = 'NIST RID'
260  ret += '\tCoexistent tag allocation authority: %s\n' % content
261 
262  elif tag == 0x50:
263  ret += '\tapplication label: %s\n' % body[2:2+tagLen].decode('utf8')
264  elif tag == 0xac:
265  ret += '\tCryptographic algorithms supported: %s\n' % body[2:2+tagLen].hex()
266  else:
267  rety += '\tunknown tag 0x%x\n' % tag
268 
269  else:
270  ret += "\tTODO: selectType %s\n" % selectT
271 
272  body = body[2+tagLen:]
273 
274  return ret
275 
276  def getData(self, fileId, bytes):
277  ret = "\tfileId=%s(%0.4x)\n" % (FIDs.get(fileId, ""), fileId)
278 
279  if len(bytes) < 7:
280  return ret + "\t/!\\ too short !!!!"
281 
282  lc = bytes[4]
283  tag = bytes[5]
284  tagLen = bytes[6]
285 
286  if lc == 4:
287  ret += "\tdoId=%0.4x\n"% (bytes[7] * 256 + bytes[8])
288 
289  elif lc == 0xa:
290  keyStr = ''
291  # TLV
292  i = 7
293  tag = bytes[i]
294  tagLen = bytes[i+1]
295  keyRef = bytes[i+3]
296  keyStr = "key(tag=0x%x len=%d ref=0x%x)=" % (tag, tagLen, keyRef)
297  i = i + 2 + tagLen
298 
299  tag = bytes[i]
300  tagLen = bytes[i+1]
301  keyStr += "value(tag=0x%x len=%d)"
302  elif lc == 3:
303  ret += "\tDiscovery Object\n"
304  elif lc == 4:
305  ret += "\tBiometric Information Templates (BIT) Group Template\n"
306  elif lc == 5:
307  if tag == 0x5C:
308  tagStr = bytes[7:10].hex()
309  ret += '\ttag: %s(%s)\n' % (tagStr, PIV_OIDs.get(tagStr, '<unknown>'))
310  self.lastGetlastGet = tagStr
311  else:
312  ret += "\tunknown key access(lc=0x%x)\n" % lc
313 
314  return ret
315 
316  def getDataResult(self, status, body):
317  ret = ''
318  if not len(body):
319  return ''
320  appTag = body[0]
321  appLen = body[1]
322 
323  body = body[2:2+appLen]
324  while len(body) > 2:
325  tag = body[0]
326  tagLen = body[1]
327  tagBody = body[2:2+tagLen]
328 
329  if self.lastGetlastGet in ('5fc102',):
330  # Card holder Unique Identifier
331  if tag == 0x30:
332  ret += '\tFASC-N: %s\n' % tagBody.hex()
333  elif tag == 0x34:
334  ret += '\tGUID: %s\n' % tagBody.hex()
335  elif tag == 0x35:
336  ret += '\texpirationDate: %s\n' % tagBody.decode('utf8')
337  elif tag == 0x3e:
338  ret += '\tIssuer Asymmetric Signature: %s\n' % tagBody.hex()
339  else:
340  ret += "\tunknown tag=0x%x len=%d content=%s\n" % (tag, tagLen, tagBody.hex())
341 
342  elif self.lastGetlastGet in ('5fc107',):
343  # Card Capability Container
344  capas = {
345  0xf0: "Card Identifier",
346  0xf1: "Capability Container version number",
347  0xf2: "Capability Grammar version number",
348  0xf3: "Applications CardURL",
349  0xf4: "PKCS#15",
350  0xf5: "Registered Data Model number",
351  0xf6: "Access Control Rule Table",
352  0xf7: "Card APDUs",
353  0xfa: "Redirection Tag",
354  0xfb: "Capability Tuples (CTs)",
355  0xfc: "Status Tuples (STs)",
356  0xfd: "Next CCC",
357  0xe3: "Extended Application CardURL",
358  0xb4: "Security Object Buffer",
359  0xfe: "Error Detection Code"
360  }
361 
362  if tag in capas.keys():
363  if tagLen:
364  ret += "\t%s: len=%d %s\n" % (capas[tag], tagLen, tagBody.hex())
365  else:
366  ret += "\tunknown capa tag 0x%x: %s\n" % (tag, tagBody.hex())
367 
368  elif self.lastGetlastGet == '5fc105':
369  # X.509 Certificate for PIV Authentication
370  pass
371 
372  else:
373  ret += "\t%s: unimplemented tag=0x%x len=%d content=%s\n" % (self.lastGetlastGet, tag, tagLen, tagBody.hex())
374 
375  body = body[2+tagLen:]
376 
377  return ret
378 
379  def getResponse(self, status, body):
380  return body.hex()
381 
382  def getResponseResult(self, status, body):
383  return body.hex()
384 
385  def mse(self, body):
386  return body.hex()
387 
388  def mseResult(self, status, body):
389  return body.hex()
390 
391  def pso(self, body):
392  return body.hex()
393 
394  def psoResult(self, status, body):
395  return body.hex()
396 
397  def verify(self, status, body):
398  return "verify(%s)" % body.hex()
399 
400  def verifyResult(self, status, body):
401  return "verify(%s)" % body.hex()
402 
403 
404 
405 
406 class ApplicationGids(object):
407  """GIDS application"""
408 
409  def __init__(self, aid):
410  self.aidaid = aid
411  self.lastDolastDo = None
412 
413  def getAID(self):
414  return self.aidaid
415 
416  def parseFcp(self, bytes):
417  ret = ''
418  tag = bytes[0]
419  tagLen = bytes[1]
420 
421  body = bytes[2:2+tagLen]
422 
423  if tag == 0x62:
424  ret += '\tFCP\n'
425 
426  while len(body) > 2:
427  tag2 = body[0]
428  tag2Len = body[1]
429  tag2Body = body[2:2+tag2Len]
430 
431  if tag2 == 0x82:
432  ret += '\t\tFileDescriptor: %s\n' % tag2Body.hex()
433  elif tag2 == 0x8a:
434  ret += '\t\tLifeCycleByte: %s\n' % tag2Body.hex()
435  elif tag2 == 0x84:
436  ret += '\t\tDF name: %s\n' % tag2Body.encode('utf8')
437  elif tag2 == 0x8C:
438  ret += '\t\tSecurityAttributes: %s\n' % tag2Body.hex()
439  else:
440  ret += '\t\tunhandled tag=0x%x body=%s\n' % (tag2, tag2Body.hex())
441 
442  body = body[2+tag2Len:]
443 
444  return ret
445 
446  def parseFci(self, bytes):
447  ret = ''
448  tag = bytes[0]
449  tagLen = bytes[1]
450 
451  body = bytes[2:2+tagLen]
452 
453  if tag == 0x61:
454  ret += '\tFCI\n'
455 
456  while len(body) > 2:
457  tag2 = body[0]
458  tag2Len = body[1]
459  tag2Body = body[2:2+tag2Len]
460 
461  if tag2 == 0x4F:
462  ret += '\t\tApplication AID: %s\n' % tag2Body.hex()
463 
464  elif tag2 == 0x50:
465  ret += '\t\tApplication label: %s\n' % tag2Body.encode('utf8')
466 
467  elif tag2 == 0x73:
468  body2 = tag2Body
469  tokens = []
470  while len(body2) > 2:
471  tag3 = body2[0]
472  tag3Len = body2[1]
473 
474  if tag3 == 0x40:
475  v = body2[2]
476  if v & 0x80:
477  tokens.append('mutualAuthSymAlgo')
478  if v & 0x40:
479  tokens.append('extAuthSymAlgo')
480  if v & 0x20:
481  tokens.append('keyEstabIntAuthECC')
482 
483 
484  body2 = body2[2+tag3Len:]
485 
486  ret += '\t\tDiscretionary data objects: %s\n' % ",".join(tokens)
487  else:
488  ret += '\t\tunhandled tag=0x%x body=%s\n' % (tag2, tag2Body.hex())
489 
490  body = body[2+tag2Len:]
491 
492  return ret
493 
494 
495  def selectResult(self, selectT, status, body):
496  if not len(body):
497  return ''
498 
499  if selectT == 'FCP':
500  return self.parseFcpparseFcp(body)
501  elif selectT == 'FCI':
502  return self.parseFciparseFci(body)
503  else:
504  return '\tselectResult(fci=%s, status=0x%x) = %s\n' % (selectT, status, body.hex())
505 
506  def getData(self, fileId, bytes):
507  lc = bytes[4]
508  tag = bytes[5]
509  tagLen = bytes[6]
510 
511  if tag == 0x5c:
512  doStr = bytes[7:7+tagLen].hex()
513  ret = '\tDO=%s\n' % DOs.get(doStr, "<%s>" % doStr)
514  self.lastDolastDo = doStr
515  else:
516  ret = '\tunknown tag=0%x len=%d v=%s' % (tag, tagLen, bytes[7:7+tagLen].hex())
517 
518  return ret
519 
520  def getDataResult(self, status, body):
521  ret = ''
522  '''
523  while len(body) > 2:
524  tag = body[0]
525  tagLen = body[1]
526 
527  ret += '\ttag=0x%x len=%d content=%s\n' % (tag, tagLen, body[2:2+tagLen].hex())
528 
529  body = body[2+tagLen:]
530  '''
531  return ret
532 
533  def mse(self, body):
534  return body.hex()
535 
536  def mseResult(self, status, body):
537  return body.hex()
538 
539  def getResponse(self, status, body):
540  return body.hex()
541 
542  def getResponseResult(self, status, body):
543  return body.hex()
544 
545  def pso(self, body):
546  return body.hex()
547 
548  def psoResult(self, status, body):
549  return body.hex()
550 
551 
552 
553 def createAppByAid(aid):
554  if aid in ("a000000308", 'a00000030800001000',):
555  return ApplicationPIV(aid)
556 
557  elif aid in ('a00000039742544659',):
558  return ApplicationGids(aid)
559 
560  elif aid in ('a0000001510000',):
561  return ApplicationGpc(aid)
562 
563  return ApplicationDummy(aid)
564 
565 
566 def getErrorCode(status):
567  if status & 0x6100:
568  return "%d more bytes" % (status & 0xff)
569 
570  return ERROR_CODES.get(status, "<unknown>")
571 
572 if __name__ == '__main__':
573  if len(sys.argv) > 1:
574  fin = open(sys.argv[1], "r")
575  else:
576  fin = sys.stdin
577 
578  lineno = 0
579  lastCmd = 0
580  lastSelect = None
581  lastSelectFCI = False
582  lastGetItem = None
583  currentApp = None
584 
585  for l in fin.readlines():
586  lineno += 1
587 
588  if not len(l):
589  continue
590 
591  # smartcard loggers have changed
592  #if l.find("[DEBUG][com.freerdp.channels.smartcard.client]") == -1:
593  # continue
594 
595  body = ''
596  recvKey = 'pbRecvBuffer: { '
597 
598  pos = l.find(recvKey)
599  if pos != -1:
600  toCard = False
601 
602  pos += len(recvKey)
603  pos2 = l.find(' }', pos)
604  if pos2 == -1:
605  print("line %d: invalid recvBuffer")
606  continue
607 
608  else:
609  toCard = True
610  sendKey = 'pbSendBuffer: { '
611  pos = l.find(sendKey)
612  if pos == -1:
613  continue
614  pos += len(sendKey)
615 
616  pos2 = l.find(' }', pos)
617  if pos2 == -1:
618  print("line %d: invalid sendBuffer")
619  continue
620 
621  body = l[pos:pos2]
622 
623  print(l[0:-1])
624  bytes = codecs.decode(body, 'hex')
625  if toCard:
626  (cla, ins, p1, p2) = bytes[0:4]
627  cmdName = CMD_NAMES.get(ins, "<COMMAND 0x%x>" % ins)
628  print(cmdName + ":")
629 
630  if cmdName == "SELECT":
631  lc = bytes[4]
632  i = 5
633 
634  if p1 == 0x00:
635  print("\tselectByFID: %0.2x%0.2x" % (bytes[i], bytes[i+1]))
636  i = i + lc
637 
638  elif p1 == 0x4:
639  aid = bytes[i:i+lc].hex()
640  lastSelect = AIDs.get(aid, '<unknown %s>' % aid)
641  print("\tselectByAID: %s(%s)" % (aid, lastSelect))
642 
643  if p2 == 0x00:
644  lastSelectT = "FCI"
645  print('\tFCI')
646  elif p2 == 0x04:
647  print('\tFCP')
648  lastSelectT = "FCP"
649  elif p2 == 0x08:
650  print('\tFMD')
651  lastSelectT = "FMD"
652 
653  if not currentApp or currentApp.getAID() != aid:
654  currentApp = createAppByAid(aid)
655 
656 
657  elif cmdName == "VERIFY":
658  lc = bytes[4]
659  P2_DATA_QUALIFIER = {
660  0x00: "Card global password",
661  0x01: "RFU",
662  0x80: "Application password",
663  0x81: "Application resetting password",
664  0x82: "Application security status resetting code",
665  }
666 
667  pin=''
668  if lc:
669  pin = ", pin='" + bytes[5:5+lc-2].decode('utf8)') + "'"
670 
671  print("\t%s%s" % (P2_DATA_QUALIFIER.get(p2, "<unknown>"), pin))
672 
673  elif cmdName == "GET DATA":
674  lc = bytes[4]
675  fileId = p1 * 256 + p2
676 
677  ret = currentApp.getData(fileId, bytes)
678  print("%s" % ret)
679 
680  elif cmdName == "GET RESPONSE":
681  #lc = bytes[4]
682  #fileId = p1 * 256 + p2
683 
684  #ret = currentApp.getResponse(fileId, bytes)
685  #print("%s" % ret)
686  pass
687  elif cmdName == "MSE":
688  ret = currentApp.mse(bytes[5:5+lc])
689  print("%s" % ret)
690 
691  elif cmdName == "PSO":
692  ret = currentApp.pso(bytes[5:5+lc])
693  print("%s" % ret)
694  else:
695  print('handle %s' % cmdName)
696 
697  lastCmd = cmdName
698 
699  else:
700  # Responses
701  if not len(bytes):
702  continue
703 
704  status = bytes[-1] + bytes[-2] * 256
705  body = bytes[0:-2]
706  print("status=0x%0.4x(%s)" % (status, getErrorCode(status)))
707 
708  if not len(body):
709  continue
710 
711  ret = ''
712  if lastCmd == "SELECT":
713  ret = currentApp.selectResult(lastSelectT, status, body)
714  elif lastCmd == "GET DATA":
715  ret = currentApp.getDataResult(status, body)
716  elif lastCmd == "MSE":
717  ret = currentApp.mseResult(status, body)
718  elif lastCmd == "PSO":
719  ret = currentApp.psoResult(status, body)
720  elif lastCmd == "GET RESPONSE":
721  ret = currentApp.getResponseResult(status, body)
722  elif lastCmd == "VERIFY":
723  ret = currentApp.verifyResult(status, body)
724 
725  if ret:
726  print("%s" % ret)