| """ |
| card: Library adapted to request (U)SIM cards and other types of telco cards. |
| Copyright (C) 2010 Benoit Michau |
| |
| This program is free software; you can redistribute it and/or modify |
| it under the terms of the GNU General Public License as published by |
| the Free Software Foundation; either version 2 of the License, or |
| (at your option) any later version. |
| |
| This program is distributed in the hope that it will be useful, |
| but WITHOUT ANY WARRANTY; without even the implied warranty of |
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| GNU General Public License for more details. |
| |
| You should have received a copy of the GNU General Public License along |
| with this program; if not, write to the Free Software Foundation, Inc., |
| 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. |
| """ |
| |
| |
| ################################# |
| # Python library to work on |
| # USIM card |
| # communication based on ISO7816 card |
| # and commands and formats based on UICC card |
| # |
| # needs pyscard from: |
| # http://pyscard.sourceforge.net/ |
| ################################# |
| |
| from card.ICC import UICC, ISO7816 |
| from card.FS import * |
| from card.utils import * |
| |
| |
| class USIM(UICC): |
| ''' |
| defines attributes, methods and facilities for ETSI / 3GPP USIM card |
| check USIM specifications in 3GPP TS 31.102 |
| |
| inherits (eventually overrides) methods and objects from UICC class |
| use self.dbg = 1 or more to print live debugging information |
| ''' |
| |
| def __init__(self): |
| ''' |
| initializes like an ISO7816-4 card with CLA=0x00 |
| and checks available AID (Application ID) read from EF_DIR |
| |
| initializes on the MF |
| ''' |
| # initialize like a UICC |
| ISO7816.__init__(self, CLA=0x00) |
| self.AID = [] |
| if self.dbg: |
| print '[DBG] type definition: %s' % type(self) |
| print '[DBG] CLA definition: %s' % hex(self.CLA) |
| |
| # USIM selection from AID |
| print '[+] UICC AID found:' |
| self.get_AID() |
| for aid in self.AID: |
| if tuple(aid[0:5]) == (0xA0, 0x00, 0x00, 0x00, 0x87) \ |
| and tuple(aid[5:7]) == (0x10, 0x02) : |
| usim = self.select( Data=aid, typ='aid') |
| if usim is None: |
| print '[+] USIM AID selection failed' |
| else: |
| print '[+] USIM AID selection succeeded\n' |
| |
| def get_imsi(self): |
| ''' |
| get_imsi() -> string(IMSI) |
| |
| reads IMSI value at address [0x6F, 0x07] |
| returns IMSI string on success or None on error |
| ''' |
| # select IMSI file |
| imsi = self.select([0x6F, 0x07]) |
| if imsi is None: |
| return None |
| # and parse the received data into the IMSI structure |
| if 'Data' in imsi.keys() and len(imsi['Data']) == 9: |
| return decode_BCD(imsi['Data'])[3:] |
| |
| # if issue with the content of the DF_IMSI file |
| if self.dbg: |
| print '[DBG] %s' % self.coms() |
| return None |
| |
| def get_CS_keys(self): |
| ''' |
| get_CS_keys() -> [KSI, CK, IK] |
| |
| reads CS UMTS keys at address [0x6F, 0x08] |
| returns list of 3 keys, each are list of bytes, on success |
| (or eventually the whole file dict if the format is strange) |
| or None on error |
| ''' |
| EF_KEYS = self.select( [0x6F, 0x08] ) |
| if self.coms()[2] == (0x90, 0x00): |
| if len(EF_KEYS['Data']) == 33: |
| KSI, CK, IK = ( EF_KEYS['Data'][0:1], |
| EF_KEYS['Data'][1:17], |
| EF_KEYS['Data'][17:33]) |
| print '[+] Successful CS keys selection: Get [KSI, CK, IK]' |
| return [KSI, CK, IK] |
| else: |
| return EF_KEYS |
| return None |
| |
| def get_PS_keys(self): |
| ''' |
| get_PS_keys() -> [KSI, CK_PS, IK_PS] |
| |
| reads PS UMTS keys at address [0x6F, 0x09] |
| returns list of 3 keys, each are list of bytes, on success |
| (or eventually the whole file dict if the format is strange) |
| or None on error |
| ''' |
| EF_KEYSPS = self.select( [0x6F, 0x09] ) |
| if self.coms()[2] == (0x90, 0x00): |
| if len(EF_KEYSPS['Data']) == 33: |
| KSI, CK, IK = ( EF_KEYSPS['Data'][0:1], |
| EF_KEYSPS['Data'][1:17], |
| EF_KEYSPS['Data'][17:33] ) |
| print '[+] Successful PS keys selection: Get [KSI, CK, IK]' |
| return [KSI, CK, IK] |
| else: |
| return EF_KEYSPS |
| return None |
| |
| def get_GBA_BP(self): |
| ''' |
| get_GBA_BP() -> [[RAND, B-TID, KeyLifetime], ...], |
| Length-Value parsing style |
| |
| reads EF_GBABP file at address [0x6F, 0xD6], |
| containing RAND and associated B-TID and KeyLifetime |
| returns list of list of bytes on success |
| (or eventually the whole file dict if the format is strange) |
| or None on error |
| ''' |
| EF_GBABP = self.select( [0x6F, 0xD6] ) |
| if self.coms()[2] == (0x90, 0x00): |
| if len(EF_GBABP['Data']) > 2: |
| #RAND, B_TID, Lifetime = LV_parser( EF_GBABP['Data'] ) |
| print '[+] Successful GBA_BP selection: Get list of ' \ |
| '[RAND, B-TID, KeyLifetime]' |
| #return (RAND, B_TID, Lifetime) |
| return LV_parser( EF_GBABP['Data'] ) |
| else: |
| return EF_GBABP |
| return None |
| |
| def update_GBA_BP(self, RAND, B_TID, key_lifetime): |
| ''' |
| update_GBA_BP([RAND], [B_TID], [key_lifetime]) |
| -> void (or EF_GBABP file dict if RAND not found) |
| |
| reads EF_GBABP file at address [0x6F, 0xD6], |
| checks if RAND provided is referenced, |
| and updates the file structure with provided B-TID and KeyLifetime |
| returns nothing (or eventually the whole file dict |
| if the RAND is not found) |
| ''' |
| GBA_BP = self.get_GBA_BP() |
| for i in GBA_BP: |
| if i == RAND: |
| print '[+] RAND found in GBA_BP' |
| # update transparent file with B_TID and key lifetime |
| self.coms.push( self.UPDATE_BINARY( P2=len(RAND)+1, |
| Data=[len(B_TID)] + B_TID + \ |
| [len(key_lifetime)] + key_lifetime )) |
| if self.dbg > 1: |
| print '[DBG] %s' % self.coms() |
| if self.coms()[2] == 0x90 and self.dbg: |
| print '[+] Successful GBA_BP update with B-TID ' \ |
| 'and key lifetime' |
| if self.dbg > 2: |
| print '[DBG] new value of EF_GBA_BP:\n%s' \ |
| % self.get_GBA_BP() |
| else: |
| if self.dbg: |
| print '[+] RAND not found in GBA_BP' |
| return GBA_BP |
| |
| def get_GBA_NL(self): |
| ''' |
| get_GBA_NL() -> [[NAF_ID, B-TID], ...] , TLV parsing style |
| |
| reads EF_GBANL file at address [0x6F, 0xDA], containing NAF_ID and B-TID |
| returns list of list of bytes vector on success |
| (or eventually the whole file dict if the format is strange) |
| or None on error |
| ''' |
| EF_GBANL = self.select( [0x6F, 0xDA] ) |
| if self.coms()[2] == (0x90, 0x00): |
| if len(EF_GBANL['Data'][0]) > 2: |
| # This is Tag-Length-Value parsing, |
| # with 0x80 for NAF_ID and 0x81 for B-TID |
| values = [] |
| |
| for rec in EF_GBANL['Data']: |
| NAF_ID, B_TID = [], [] |
| while len(rec) > 0: |
| tlv = first_TLV_parser( rec ) |
| if tlv[1] > 0xFF: |
| rec = rec[ tlv[1]+4 : ] |
| else: |
| rec = rec[ tlv[1]+2 : ] |
| if tlv[0] == 0x80: |
| NAF_ID = tlv[2] |
| elif tlv[0] == 0x81: |
| B_TID = tlv[2] |
| values.append( [NAF_ID, B_TID] ) |
| |
| print '[+] Successful GBA_NL selection: ' \ |
| 'Get list of [NAF_ID, B-TID]' |
| #return (NAF_ID, B_TID) |
| return values |
| else: |
| return EF_GBANL |
| return None |
| |
| def authenticate(self, RAND=[], AUTN=[], ctx='3G'): |
| ''' |
| self.authenticate(RAND, AUTN, ctx='3G') -> [key1, key2...], |
| LV parsing style |
| |
| runs the INTERNAL AUTHENTICATE command in the USIM |
| with the right context: |
| ctx = '2G', '3G', 'GBA' ('MBMS' or other not supported at this time) |
| RAND and AUTN are list of bytes; for '2G' context, AUTN is not used |
| returns a list containing the keys (list of bytes) computed in the USIM, |
| on success: |
| [RES, CK, IK (, Kc)] or [AUTS] for '3G' |
| [RES] or [AUTS] for 'GBA' |
| [RES, Kc] for '2G' |
| or None on error |
| ''' |
| # prepare input data for authentication |
| if ctx in ('3G', 'VGCS', 'GBA', 'MBMS') and len(RAND) != 16 \ |
| and len(AUTN) != 16: |
| print '[ERR] missing parameters or wrong length' |
| return None |
| |
| inp = [] |
| if ctx == '3G': |
| P2 = 0x81 |
| elif ctx == 'VGCS': |
| P2 = 0x82 |
| print '[+] Not implemented. Exit.' |
| return None |
| elif ctx == 'MBMS': |
| print '[+] Not implemented. Exit.' |
| return None |
| elif ctx == 'GBA': |
| P2 = 0x84 |
| inp = [0xDD] |
| inp.extend( [len(RAND)] + RAND + [len(AUTN)] + AUTN ) |
| if ctx not in ['3G', 'VGCS', 'MBMS', 'GBA']: |
| # and also, if ctx == '2G'... the safe way |
| # to avoid desynchronizing our USIM counter |
| P2 = 0x80 |
| if len(RAND) != 16: |
| print '[ERR] RAND has wrong length (%d, should be 16)' % len(RAND) |
| return None |
| # override input value for 2G authent |
| inp = [len(RAND)] + RAND |
| |
| self.coms.push( self.INTERNAL_AUTHENTICATE(P2=P2, Data=inp) ) |
| if self.coms()[2][0] in (0x9F, 0x61): |
| self.coms.push( self.GET_RESPONSE(Le=self.coms()[2][1]) ) |
| if self.coms()[2] == (0x90, 0x00): |
| val = self.coms()[3] |
| if P2 == 0x80: |
| if self.dbg: |
| print '[+] Successful 2G authentication. Get [RES, Kc]' |
| values = LV_parser(val) |
| # returned values are (RES, Kc) |
| return values |
| # not adapted to 2G context with Kc, RES: to be confirmed... |
| if val[0] == 0xDB: |
| if self.dbg: |
| if P2 == 0x81: |
| print '[+] Successful 3G authentication. ' \ |
| 'Get [RES, CK, IK(, Kc)]' |
| elif P2 == 0x84: |
| print '[+] Successful GBA authentication. Get [RES]' |
| else: |
| print '[+] response: %s' % hex(val) |
| values = LV_parser(val[1:]) |
| # returned values can be (RES, CK, IK) or (RES, CK, IK, Kc) |
| return values |
| elif val[0] == 0xDC: |
| if self.dbg: |
| print '[+] Synchronization failure. Get [AUTS]' |
| values = LV_parser(val[1:]) |
| return values |
| elif self.dbg: |
| print '[+] response: %s' % hex(val) |
| #else: |
| if self.dbg: |
| print '[+] authentication error: %s' % self.coms() |
| return None |
| |
| def GBA_derivation(self, NAF_ID=[], IMPI=[]): |
| ''' |
| self.GBA_derivation(NAF_ID, IMPI) -> [Ks_ext_naf] |
| |
| runs the INTERNAL AUTHENTICATE command in the USIM |
| with the GBA derivation context: |
| NAF_ID is a list of bytes (use stringToByte()) |
| "NAF domain name"||"security protocol id", |
| eg: "application.org"||"0x010001000a" (> TLS with RSA and SHA) |
| IMPI is a list of bytes |
| "IMSI@ims.mncXXX.mccYYY.3gppnetwork.org" if no IMS IMPI |
| is specifically defined in the USIM |
| returns a list with GBA ext key (list of bytes) computed in the USIM: |
| [Ks_ext_naf] |
| Ks_int_naf remains available in the USIM |
| for further GBA_U key derivation |
| or None on error |
| |
| see TS 33.220 for GBA specific formats |
| ''' |
| # need to run 1st an authenicate command with 'GBA' context, |
| # so to have the required keys in the USIM |
| P2 = 0x84 |
| inp = [0xDE] + [len(NAF_ID)] + NAF_ID + [len(IMPI)] + IMPI |
| |
| self.coms.push( self.INTERNAL_AUTHENTICATE(P2=P2, Data=inp) ) |
| if self.coms()[2][0] in (0x9F, 0x61): |
| self.coms.push( self.GET_RESPONSE(Le=self.coms()[2][1]) ) |
| if self.coms()[2] == (0x90, 0x00): |
| val = self.coms()[3] |
| if val[0] == 0xDB: # not adapted to 2G context with Kc, RES |
| if self.dbg: |
| print '[+] Successful GBA derivation. Get [Ks_EXT_NAF]' |
| values = LV_parser(val[1:]) |
| return values |
| if self.dbg: |
| print '[DBG] authentication failure: %s' % self.coms() |
| return None |
| |
| def bf_FS_from_init( self, filename='bf_USIM', file_dict=USIM_app_FS, |
| init_method='select_by_aid', init_args=[1] ): |
| ''' |
| bruteforces the USIM filesystem at the application initialization level: |
| thanks to UICC.select_by_aid(1) |
| could be used another way... |
| stores the result in the file passed in argument |
| (file will be overwritten) |
| |
| TODO: does not manage file recursivity (when entering DF) |
| only scan 1st level files |
| ''' |
| fd = open(filename, 'w') |
| # loop on all possible addresses |
| for i in range(0x00, 0xff): |
| for j in range(0x00, 0xff): |
| # here "ret" is useless, |
| # but calling the method places the smartcard |
| # at the right address (by default: 1st AID) |
| ret = getattr(self, init_method)(*init_args) |
| fil = self.select( [i, j] ) |
| # if file characteristics is readable |
| if fil is not None: |
| if self.dbg: |
| print '[+] USIM file found at address %s %s' % (i, j) |
| # warning when working with DF recursivity |
| if (i, j) in file_dict.keys(): |
| fil['name'] = file_dict[(i, j)][0] |
| k = fil.keys() |
| k.sort() |
| fd.write('\n') |
| for key in k: |
| fd.write('%s: %s\n' % (key, fil[key])) |
| # if file exists but special conditions are returned |
| if self.coms()[2] not in ((0x6A, 0x82), (0x90, 0x00)): |
| if self.dbg: |
| print '[+] special condition %s when selecting' \ |
| ' / reading USIM file at address %s %s' \ |
| % ( self.coms()[2], i, j ) |
| fd.write('\n') |
| fd.write('file exists at address: %s %s\n' \ |
| % (hex(i)[2:], hex(j)[2:])) |
| fd.write('%s\n' % self.coms()[1]) |
| |
| if self.dbg and (i % 0x10 == 0 and j % 0x40 == 0): |
| print '[-] going over address %s %s' % (i, j) |
| |
| fd.write('\n') |
| fd.close() |
| |
| def bf_FS_from_MF(self, filename): |
| ''' |
| not implemented |
| ''' |
| #bf from MF, and recursively under each DF |
| #bf from each AID and recursively under each DF in AID |
| pass |
| |
| # vim: tabstop=4 shiftwidth=4 expandtab |