aboutsummaryrefslogtreecommitdiffstats
path: root/card/USIM.py
blob: d8cbb850a3502ab87c95122312ab755aa9edbe82 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
"""
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: 
            if self.dbg: 
                print '[WNG] authenticate: bad parameters'
            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: 
                if self.dbg: 
                    print '[WNG] bad parameters'
                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 P2 == 0x81 and self.dbg: 
                        print '[+] Successful 3G authentication. ' \
                              'Get [RES, CK, IK(, Kc)]' 
                    elif P2 == 0x84 and self.dbg: 
                        print '[+] Successful GBA authentication. Get [RES]'
                    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
        #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