diff options
author | Harald Welte <laforge@gnumonks.org> | 2011-12-07 01:51:46 +0100 |
---|---|---|
committer | Harald Welte <laforge@gnumonks.org> | 2011-12-07 01:51:46 +0100 |
commit | c1cd027c8a65fc255f0d2271e06c525e68ada164 (patch) | |
tree | 83d01e854b39cf181f50fc3f671a2aa2c702ba26 |
Initial check-in of a small command line based tool for (U)SIM auth
-rw-r--r-- | README | 165 | ||||
-rw-r--r-- | card/FS.py | 303 | ||||
-rw-r--r-- | card/ICC.py | 1483 | ||||
-rw-r--r-- | card/SIM.py | 274 | ||||
-rw-r--r-- | card/USIM.py | 402 | ||||
-rw-r--r-- | card/__init__.py | 27 | ||||
-rw-r--r-- | card/utils.py | 252 | ||||
-rwxr-xr-x | osmo-sim-auth.py | 106 |
8 files changed, 3012 insertions, 0 deletions
@@ -0,0 +1,165 @@ += osmo-sim-auth = + +This is a small script that can be used with a PC-based smart card +reader to obtain GSM/UMTS authentication parameters from a SIM/USIM +card. + +== prerequisites == + +We assume that you have + +* A smart card reader compatible with pcsc-lite +* Installed python program and pyscard library + + +=== smart card reader === + +Any reader supported by pcsc-lite will work. However, a reader +compatible with the USB CCID device class is much recommended. + +Please verify that the hardware and driver setup is working, e.g. by +using the 'pcsc_scan' tool included with pcsc-lite. You should get an +output like: +{{{ +V 1.4.17 (c) 2001-2009, Ludovic Rousseau <ludovic.rousseau@free.fr> +Compiled with PC/SC lite version: 1.5.5 +Scanning present readers... +0: OmniKey CardMan 5121 00 00 + +Wed Dec 7 01:32:37 2011 + Reader 0: OmniKey CardMan 5121 00 00 + Card state: Card inserted, Shared Mode, + ATR: 3B 9F 95 80 1F C7 80 31 E0 73 FE 21 13 57 12 29 11 02 01 00 00 C2 + +ATR: 3B 9F 95 80 1F C7 80 31 E0 73 FE 21 13 57 12 29 11 02 01 00 00 C2 +}}} + +plus many more lines of output decoding the ATR. + +If you only get +{{{ +PC/SC device scanner +V 1.4.17 (c) 2001-2009, Ludovic Rousseau <ludovic.rousseau@free.fr> +Compiled with PC/SC lite version: 1.5.5 +Scanning present readers... +0: OmniKey CardMan 5121 00 00 + +Wed Dec 7 01:35:08 2011 + Reader 0: OmniKey CardMan 5121 00 00 + Card state: Card removed, +}}} + +then your card was not detected in the reader. + +If you don't even get any displayed readers, your hardware and/or driver +setup are likely wrong. + + +=== pyscard === + +pyscard can be installed from packages of major Linux distributions. + +If you want to build it from source, it is available from +http://pyscard.sourceforge.net/ + + +== running osmo-sim-auth == + +{{{ +$ ./osmo-sim-auth.py --help +Usage: osmo-sim-auth.py [options] + +Options: + -h, --help show this help message and exit + -a AUTN, --autn=AUTN AUTN parameter from AuC + -r RAND, --rand=RAND RAND parameter from AuC + -d, --debug Enable debug output + -s, --sim SIM mode (default: USIM) +}}} + +you can run the program in two modes: + * running GSM authentication (classic SIM card protocol) + * running UMTS authentication (USIM card protocol) + +=== classic GSM authentication === + +This mode will use the "RUN GSM ALGORITHM" command as specified in GMS +TS 11.11 + +You have to specify + * the 16 byte RAND value from the AuC (-r) as 32 hex digits + * the '-s' flag to enable SIM mode + +{{{ +$ ./osmo-sim-auth.py -r 00000000000000000000000000000000 -s +Testing SIM card with IMSI 901700000000403 + +GSM Authentication +SRES: 215fdb4d +Kc: 6de816a759a42912 +}}} + +=== UMTS authentication === + +This mode will use the "AUTHENTICATE" command as specified in 3GPP TS +31.102 + +You have to specify + * the 16 byte RAND value from the AuC (-r) as 32 hex digits + * the 16 byte AUTN value from the AuC (-a) as 32 hex digits + +==== successful operation ==== + +In this case, the tool will output the following values obtained from +the card: + * RES authentication result value + * CK ciphering key + * IK integrity key + * Kc for inter-RAN handover from UMTS -> 2G + +Secondly, the tool will re-run the authentication in "2G authentication +context" in order to obtain the SRES result. This value would be used +if a 3G/2G dual-mode phone registers on a 2G network. + +{{{ +python ./osmo-sim-auth.py -r 00000000000000000000000000000000 -a ec9320c2c2000000e1dd22c1ad3e2d3d +[+] UICC AID found: +found [AID 1] 3GPP || USIM || (255, 134) || (255, 255) || (137, 255, +255, 255, 255) +[+] USIM AID selection succeeded + +Testing USIM card with IMSI 901700000000403 + +UMTS Authentication +RES: e9fc88ccc8a35381 +CK: 7200a184d8f2c758fbdf87900ddbf275 +IK: 12cb2dd3e0ec8378f6fc1d606c619f47 +Kc: 6de816a759a42912 + +GSM Authentication +SRES: 215fdb4d +Kc: 6de816a759a42912 +}}} + +==== synchronization required ==== + +In this case, the AUTHENTICATE command will return the AUTS parameter, +which has to be sent to the AuC in order to re-synchronzie the SQN +counter which is kept in both the USIM as well as the AuC. + +{{{ +./osmo-sim-auth.py -r 00000000000000000000000000000000 -a ec9320c2c2120000c8b7de2a3449f1bd +[+] UICC AID found: +found [AID 1] 3GPP || USIM || (255, 134) || (255, 255) || (137, 255, +255, 255, 255) +[+] USIM AID selection succeeded + +Testing USIM card with IMSI 901700000000403 + +UMTS Authentication +AUTS: 8711a0ec9e2be2f766881a64605b + +GSM Authentication +SRES: 215fdb4d +Kc: 6de816a759a42912 +}}} diff --git a/card/FS.py b/card/FS.py new file mode 100644 index 0000000..2d51e2b --- /dev/null +++ b/card/FS.py @@ -0,0 +1,303 @@ +""" +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. +""" + +################################# +# 3GPP SIM and USIM File-System # +# see TS 51.11 for SIM # +# see TS 31.102 for USIM # +################################# + +# (U)SIM file-system dictionnaries +# (absolut_file_address) : 'file_name' + +SIM_FS = { +(0x3F, 0x00) : 'MF', +(0x2F, 0xE2) : 'EF_ICCID', +(0x2F, 0x05) : 'EF_ELP', +(0x7F, 0x23) : 'DF_FP-CTS', +(0x7F, 0x22) : 'DF_IS-41', +(0x7F, 0x10) : 'DF_TELECOM', +(0x7F, 0x10, 0x6F, 0x3A) : 'EF_ADN', +(0x7F, 0x10, 0x6F, 0x3B) : 'EF_FDN', +(0x7F, 0x10, 0x6F, 0x3C) : 'EF_SMS', +(0x7F, 0x10, 0x6F, 0x3D) : 'EF_CCP', +(0x7F, 0x10, 0x6F, 0x40) : 'EF_MSISDN', +(0x7F, 0x10, 0x6F, 0x42) : 'EF_SMSP', +(0x7F, 0x10, 0x6F, 0x43) : 'EF_SMSS', +(0x7F, 0x10, 0x6F, 0x44) : 'EF_LND', +(0x7F, 0x10, 0x6F, 0x47) : 'EF_SMSR', +(0x7F, 0x10, 0x6F, 0x49) : 'EF_SDN', +(0x7F, 0x10, 0x6F, 0x4A) : 'EF_EXT1', +(0x7F, 0x10, 0x6F, 0x4B) : 'EF_EXT2', +(0x7F, 0x10, 0x6F, 0x4C) : 'EF_EXT3', +(0x7F, 0x10, 0x6F, 0x4D) : 'EF_BDN', +(0x7F, 0x10, 0x6F, 0x4E) : 'EF_EXT4', +(0x7F, 0x10, 0x6F, 0x58) : 'EF_CMI', +(0x7F, 0x10, 0x6F, 0x4F) : 'EF_ECCP', +(0x7F, 0x10, 0x5F, 0x50) : 'DF_GRAPHICS', +(0x7F, 0x10, 0x5F, 0x50, 0x4F, 0x20) : 'EF_IMG', +(0x7F, 0x20) : 'DF_GSM', +(0x7F, 0x20, 0x5F, 0x3C) : 'DF_MExE', +(0x7F, 0x20, 0x5F, 0x3C, 0x4F, 0x40) : 'EF_MExE-ST', +(0x7F, 0x20, 0x5F, 0x3C, 0x4F, 0x41) : 'EF_ORPK', +(0x7F, 0x20, 0x5F, 0x3C, 0x4F, 0x42) : 'EF_ARPK', +(0x7F, 0x20, 0x5F, 0x3C, 0x4F, 0x43) : 'EF_TPRPK', +(0x7F, 0x20, 0x5F, 0x30) : 'DF_IRIDIUM', +(0x7F, 0x20, 0x5F, 0x31) : 'DF_GLOBST', +(0x7F, 0x20, 0x5F, 0x32) : 'DF_ICO', +(0x7F, 0x20, 0x5F, 0x33) : 'DF_ACeS', +(0x7F, 0x20, 0x5F, 0x40) : 'DF_EIA/TIA-553', +(0x7F, 0x20, 0x5F, 0x60) : 'DF_CTS', +(0x7F, 0x20, 0x5F, 0x70) : 'DF_SoLSA', +(0x7F, 0x20, 0x5F, 0x70, 0x4F, 0x30) : 'EF_SAI', +(0x7F, 0x20, 0x5F, 0x70, 0x4F, 0x31) : 'EF_SLL', +(0x7F, 0x20, 0x6F, 0x05) : 'EF_LP', +(0x7F, 0x20, 0x6F, 0x07) : 'EF_IMSI', +(0x7F, 0x20, 0x6F, 0x20) : 'EF_Kc', +(0x7F, 0x20, 0x6F, 0x2C) : 'EF_DCK', +(0x7F, 0x20, 0x6F, 0x30) : 'EF_PLMNsel', +(0x7F, 0x20, 0x6F, 0x31) : 'EF_HPPLMN', +(0x7F, 0x20, 0x6F, 0x32) : 'EF_CNL', +(0x7F, 0x20, 0x6F, 0x37) : 'EF_ACMmax', +(0x7F, 0x20, 0x6F, 0x38) : 'EF_SST', +(0x7F, 0x20, 0x6F, 0x39) : 'EF_ACM', +(0x7F, 0x20, 0x6F, 0x3E) : 'EF_GID1', +(0x7F, 0x20, 0x6F, 0x3F) : 'EF_GID2', +(0x7F, 0x20, 0x6F, 0x41) : 'EF_PUCT', +(0x7F, 0x20, 0x6F, 0x45) : 'EF_CBMI', +(0x7F, 0x20, 0x6F, 0x46) : 'EF_SPN', +(0x7F, 0x20, 0x6F, 0x48) : 'EF_CBMID', +(0x7F, 0x20, 0x6F, 0x74) : 'EF_BCCH', +(0x7F, 0x20, 0x6F, 0x78) : 'EF_ACC', +(0x7F, 0x20, 0x6F, 0x7B) : 'EF_FPLMN', +(0x7F, 0x20, 0x6F, 0x7E) : 'EF_LOCI', +(0x7F, 0x20, 0x6F, 0xAD) : 'EF_AD', +(0x7F, 0x20, 0x6F, 0xAE) : 'EF_PHASE', +(0x7F, 0x20, 0x6F, 0xB1) : 'EF_VGCS', +(0x7F, 0x20, 0x6F, 0xB2) : 'EF_VGCSS', +(0x7F, 0x20, 0x6F, 0xB3) : 'EF_VBS', +(0x7F, 0x20, 0x6F, 0xB4) : 'EF_VBSS', +(0x7F, 0x20, 0x6F, 0xB5) : 'EF_eMLPP', +(0x7F, 0x20, 0x6F, 0xB6) : 'EF_AAeM', +(0x7F, 0x20, 0x6F, 0xB7) : 'EF_ECC', +(0x7F, 0x20, 0x6F, 0x50) : 'EF_CBMIR', +(0x7F, 0x20, 0x6F, 0x51) : 'EF_NIA', +(0x7F, 0x20, 0x6F, 0x52) : 'EF_KcGPRS', +(0x7F, 0x20, 0x6F, 0x53) : 'EF_LOCIGPRS', +(0x7F, 0x20, 0x6F, 0x54) : 'EF_SUME', +(0x7F, 0x20, 0x6F, 0x60) : 'EF_PLMNwAcT', +(0x7F, 0x20, 0x6F, 0x61) : 'EF_OPLMNwAcT', +(0x7F, 0x20, 0x6F, 0x62) : 'EF_HPLMNAcT', +(0x7F, 0x20, 0x6F, 0x63) : 'EF_CPBCCH', +(0x7F, 0x20, 0x6F, 0x64) : 'EF_INVSCAN', +(0x7F, 0x20, 0x6F, 0xC5) : 'EF_PNN', +(0x7F, 0x20, 0x6F, 0xC6) : 'EF_OPL', +(0x7F, 0x20, 0x6F, 0xC7) : 'EF_MBDN', +(0x7F, 0x20, 0x6F, 0xC8) : 'EF_EXT6', +(0x7F, 0x20, 0x6F, 0xC9) : 'EF_MBI', +(0x7F, 0x20, 0x6F, 0xCA) : 'EF_MWIS', +(0x7F, 0x20, 0x6F, 0xCB) : 'EF_CFIS', +(0x7F, 0x20, 0x6F, 0xCC) : 'EF_EXT7', +(0x7F, 0x20, 0x6F, 0xCD) : 'EF_SPDI', +(0x7F, 0x20, 0x6F, 0xCE) : 'EF_MMSN', +(0x7F, 0x20, 0x6F, 0xCF) : 'EF_EXT8', +(0x7F, 0x20, 0x6F, 0xD0) : 'EF_MMSICP', +(0x7F, 0x20, 0x6F, 0xD1) : 'EF_MMSUP', +(0x7F, 0x20, 0x6F, 0xD2) : 'EF_MMSUCP', +} + +USIM_FS = { +(0x3F, 0x00) : 'MF', +(0x2F, 0x00) : 'EF_DIR', +(0x2F, 0x05) : 'EF_PL', +(0x2F, 0x06) : 'EF_ARR', +(0x2F, 0xE2) : 'EF_ICCID', +(0x7F, 0x20) : 'DF_GSM', +(0x7F, 0x10) : 'DF_TELECOM', +(0x7F, 0x10, 0x6F, 0x06) : 'EF_ARR', +(0x7F, 0x10, 0x6F, 0x3A) : 'EF_ADN', +(0x7F, 0x10, 0x6F, 0x3B) : 'EF_FDN', +(0x7F, 0x10, 0x6F, 0x3C) : 'EF_SMS', +(0x7F, 0x10, 0x6F, 0x4F) : 'EF_ECCP', +(0x7F, 0x10, 0x6F, 0x40) : 'EF_MSISDN', +(0x7F, 0x10, 0x6F, 0x42) : 'EF_SMSP', +(0x7F, 0x10, 0x6F, 0x43) : 'EF_SMSS', +(0x7F, 0x10, 0x6F, 0x44) : 'EF_LND', +(0x7F, 0x10, 0x6F, 0x47) : 'EF_SMSR', +(0x7F, 0x10, 0x6F, 0x49) : 'EF_SDN', +(0x7F, 0x10, 0x6F, 0x4A) : 'EF_EXT1', +(0x7F, 0x10, 0x6F, 0x4B) : 'EF_EXT2', +(0x7F, 0x10, 0x6F, 0x4C) : 'EF_EXT3', +(0x7F, 0x10, 0x6F, 0x4D) : 'EF_BDN', +(0x7F, 0x10, 0x6F, 0x4E) : 'EF_EXT4', +(0x7F, 0x10, 0x6F, 0x54) : 'EF_SUME', +(0x7F, 0x10, 0x5F, 0x50) : 'DF_GRAPHICS', +(0x7F, 0x10, 0x5F, 0x50, 0x4F, 0x20) : 'EF_IMG', +#(0x7F, 0x10, 0x5F, 0x50, 0x4F, 0xXX) : 'EF_IIDFn', +(0x5F, 0x3A) : 'DF_PHONEBOOK', +(0x5F, 0x3A, 0x4F, 0x30) : 'EF_PBR', +#(0x5F, 0x3A, 0x4F, 0xXX) : 'EF_IAP', +#(0x5F, 0x3A, 0x4F, 0xXX) : 'EF_ADN', +#(0x5F, 0x3A, 0x4F, 0xXX) : 'EF_EXT1', +#(0x5F, 0x3A, 0x4F, 0xXX) : 'EF_PBC', +#(0x5F, 0x3A, 0x4F, 0xXX) : 'EF_GRP', +#(0x5F, 0x3A, 0x4F, 0xXX) : 'EF_AAS', +#(0x5F, 0x3A, 0x4F, 0xXX) : 'EF_GAS', +#(0x5F, 0x3A, 0x4F, 0xXX) : 'EF_ANR', +#(0x5F, 0x3A, 0x4F, 0xXX) : 'EF_SNE', +#(0x5F, 0x3A, 0x4F, 0xXX) : 'EF_CCP1', +#(0x5F, 0x3A, 0x4F, 0xXX) : 'EF_UID', +(0x5F, 0x3A, 0x4F, 0x22) : 'EF_PSC', +(0x5F, 0x3A, 0x4F, 0x23) : 'EF_CC', +(0x5F, 0x3A, 0x4F, 0x24) : 'EF_PUID', +#(0x5F, 0x3A, 0x4F, 0xXX) : 'EF_EMAIL', +(0x5F, 0x3B) : 'DF_MULTIMEDIA', +(0x5F, 0x3B, 0x4F, 0x47) : 'EF_MML', +(0x5F, 0x3B, 0x4F, 0x48) : 'EF_MMDF', +} + + +USIM_app_FS = { +(0x5F, 0x40) : 'DF_WLAN', +(0x5F, 0x40, 0x4F, 0x41) : 'EF_Pseudo', +(0x5F, 0x40, 0x4F, 0x42) : 'EF_UPLMNWLAN', +(0x5F, 0x40, 0x4F, 0x43) : 'EF_0PLMNWLAN', +(0x5F, 0x40, 0x4F, 0x44) : 'EF_USSIDL', +(0x5F, 0x40, 0x4F, 0x45) : 'EF_OSSIDL', +(0x5F, 0x40, 0x4F, 0x46) : 'EF_WRI', +(0x5F, 0x70) : 'DF_SoLSA', +(0x5F, 0x70, 0x4F, 0x30) : 'EF_SAI', +(0x5F, 0x70, 0x4F, 0x31) : 'EF_SLL', +(0x5F, 0x3C) : 'DF_MExE', +(0x5F, 0x3C, 0x4F, 0x40) : 'EF_MExE-ST', +(0x5F, 0x3C, 0x4F, 0x41) : 'EF_ORPK', +(0x5F, 0x3C, 0x4F, 0x42) : 'EF_ARPK', +(0x5F, 0x3C, 0x4F, 0x43) : 'EF_TPRK', +#(0x5F, 0x3C, 0x4F, 0xXX) : 'EF_TKCDF', +(0x5F, 0x3B) : 'DF_GSM-ACCESS', +(0x5F, 0x3B, 0x4F, 0x20) : 'EF_Kc', +(0x5F, 0x3B, 0x4F, 0x52) : 'EF_KcGPRS', +(0x5F, 0x3B, 0x4F, 0x63) : 'EF_CPBCCH', +(0x5F, 0x3B, 0x4F, 0x64) : 'EF_invSCAN', +(0x5F, 0x3A) : 'DF_PHONEBOOK', +#(0x5F, 0x3A, 0x4F, 0xXX) : 'EF_UID', +(0x5F, 0x3A, 0x4F, 0x22) : 'EF_PSC', +(0x5F, 0x3A, 0x4F, 0x23) : 'EF_CC', +(0x5F, 0x3A, 0x4F, 0x24) : 'EF_PUID', +(0x5F, 0x3A, 0x4F, 0x30) : 'EF_PBR', +#(0x5F, 0x3A, 0x4F, 0xXX) : 'EF_CCP1', +#(0x5F, 0x3A, 0x4F, 0xXX) : 'EF_IAP', +#(0x5F, 0x3A, 0x4F, 0xXX) : 'EF_ADN', +#(0x5F, 0x3A, 0x4F, 0xXX) : 'EF_EXT1', +#(0x5F, 0x3A, 0x4F, 0xXX) : 'EF_PBC', +#(0x5F, 0x3A, 0x4F, 0xXX) : 'EF_GRP', +#(0x5F, 0x3A, 0x4F, 0xXX) : 'EF_AAS', +#(0x5F, 0x3A, 0x4F, 0xXX) : 'EF_GAS', +#(0x5F, 0x3A, 0x4F, 0xXX) : 'EF_ANR', +#(0x5F, 0x3A, 0x4F, 0xXX) : 'EF_SNE', +#(0x5F, 0x3A, 0x4F, 0xXX) : 'EF_EMAIL', +(0x6F, 0x05) : 'EF_LI', +(0x6F, 0x06) : 'EF_ARR', +(0x6F, 0x07) : 'EF_IMSI', +(0x6F, 0x08) : 'EF_Keys', +(0x6F, 0x09) : 'EF_KeysPS', +(0x6F, 0x2C) : 'EF_DCK', +(0x6F, 0x31) : 'EF_HPPLMN', +(0x6F, 0x32) : 'EF_CNL', +(0x6F, 0x37) : 'EF_ACMmax', +(0x6F, 0x38) : 'EF_UST', +(0x6F, 0x39) : 'EF_ACM', +(0x6F, 0x3B) : 'EF_FDN', +(0x6F, 0x3C) : 'EF_SMS', +(0x6F, 0x3E) : 'EF_GID1', +(0x6F, 0x3F) : 'EF_GID2', +(0x6F, 0x40) : 'EF_MSISDN', +(0x6F, 0x41) : 'EF_PUCT', +(0x6F, 0x42) : 'EF_SMSP', +(0x6F, 0x43) : 'EF_SMSS', +(0x6F, 0x45) : 'EF_CBMI', +(0x6F, 0x46) : 'EF_SPN', +(0x6F, 0x47) : 'EF_SMSR', +(0x6F, 0x48) : 'EF_CBMID', +(0x6F, 0x49) : 'EF_SDN', +(0x6F, 0x4B) : 'EF_EXT2', +(0x6F, 0x4C) : 'EF_EXT3', +(0x6F, 0x4D) : 'EF_BDN', +(0x6F, 0x4E) : 'EF_EXT5', +(0x6F, 0x50) : 'EF_CBMIR', +(0x6F, 0x55) : 'EF_EXT4', +(0x6F, 0x56) : 'EF_EST', +(0x6F, 0x57) : 'EF_ACL', +(0x6F, 0x58) : 'EF_CMI', +(0x6F, 0x5B) : 'EF_START-HFN', +(0x6F, 0x5C) : 'EF_THRESHOLD', +(0x6F, 0x60) : 'EF_PLMNwAcT', +(0x6F, 0x61) : 'EF_OPLMNwAcT', +(0x6F, 0x62) : 'EF_HPLMNwAcT', +(0x6F, 0xD9) : 'EF_EHPLMN', +(0x6F, 0x73) : 'EF_PSLOCI', +(0x6F, 0x78) : 'EF_ACC', +(0x6F, 0x7B) : 'EF_FPLMN', +(0x6F, 0x7E) : 'EF_LOCI', +(0x6F, 0x80) : 'EF_ICI', +(0x6F, 0x81) : 'EF_OCI', +(0x6F, 0x82) : 'EF_ICT', +(0x6F, 0x83) : 'EF_OCT', +(0x6F, 0xAD) : 'EF_AD', +(0x6F, 0xB5) : 'EF_eMLPP', +(0x6F, 0xB6) : 'EF_AAeM', +(0x6F, 0xB7) : 'EF_ECC', +(0x6F, 0xC3) : 'EF_Hiddenkey', +(0x6F, 0xC4) : 'EF_NETPAR', +(0x6F, 0xC5) : 'EF_PNN', +(0x6F, 0xC6) : 'EF_OPL', +(0x6F, 0xC7) : 'EF_MBDN', +(0x6F, 0xC8) : 'EF_EXT6', +(0x6F, 0xC9) : 'EF_MBI', +(0x6F, 0xCA) : 'EF_MWIS', +(0x6F, 0xCB) : 'EF_CFIS', +(0x6F, 0xCC) : 'EF_EXT7', +(0x6F, 0xCD) : 'EF_SPDI', +(0x6F, 0xCE) : 'EF_MMSN', +(0x6F, 0xCF) : 'EF_EXT8', +(0x6F, 0xD0) : 'EF_MMSICP', +(0x6F, 0xD1) : 'EF_MMSUP', +(0x6F, 0xD2) : 'EF_MMSUCP', +(0x6F, 0xD3) : 'EF_NIA', +(0x6F, 0x4F) : 'EF_CCP2', +(0x6F, 0xB1) : 'EF_VGCS', +(0x6F, 0xB2) : 'EF_VGCSS', +(0x6F, 0xB3) : 'EF_VBS', +(0x6F, 0xB4) : 'EF_VBSS', +(0x6F, 0xD4) : 'EF_VGCSCA', +(0x6F, 0xD5) : 'EF_VBSCA', +(0x6F, 0xD6) : 'EF_GBAP', +(0x6F, 0xD7) : 'EF_MSK', +(0x6F, 0xD8) : 'EF_MUK', +(0x6F, 0xDA) : 'EF_GBANL', +(0x6F, 0xDB) : 'EF_EHPLMNPI', +(0x6F, 0xDC) : 'EF_LRPLMNSI', +(0x6F, 0xDD) : 'EF_NAFKCA', +(0x6F, 0xDE) : 'EF_SPNI', +(0x6F, 0xDF) : 'EF_PNNI', +} + +EMV_AID = { +(160, 0, 0, 0, 3, 16, 16) : 'VISA credit / debit', +}
\ No newline at end of file diff --git a/card/ICC.py b/card/ICC.py new file mode 100644 index 0000000..a9cfc88 --- /dev/null +++ b/card/ICC.py @@ -0,0 +1,1483 @@ +""" +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 +# smartcard defined with ISO 7816 +# +# Specially designed SIM and USIM class +# for ETSI / 3GPP cards +# +# needs pyscard from: +# http://pyscard.sourceforge.net/ +################################# + +# classic python modules +import os +import re + +# smartcard python modules from pyscard +from smartcard.CardType import AnyCardType +from smartcard.CardRequest import CardRequest +from smartcard.CardConnection import CardConnection +from smartcard.ATR import ATR +from smartcard.Exceptions import CardConnectionException +from smartcard.util import toHexString + +from card.utils import * + +########################################################### +# ISO7816 class with attributes and methods as defined +# by ISO-7816 part 4 standard for smartcard +########################################################### + +class ISO7816(object): + ''' + define attributes, methods and facilities for ISO-7816-4 standard smartcard + + use self.dbg = 1 or more to print live debugging information + standard instructions codes available in "INS_dic" class dictionnary + standard file tags available in "file_tags" class dictionnary + ''' + + dbg = 0 + + INS_dic = { + 0x04 : 'DEACTIVATE FILE', + 0x0C : 'ERASE RECORD(S)', + 0x0E : 'ERASE BINARY', + 0x0F : 'ERASE BINARY', + 0x10 : 'TERMINAL PROFILE', + 0x12 : 'FETCH', + 0x14 : 'TERMINAL RESPONSE', + 0x20 : 'VERIFY', + 0x21 : 'VERIFY', + 0x22 : 'MANAGE SECURITY ENVIRONMENT', + 0x24 : 'CHANGE PIN', + 0x26 : 'DISABLE PIN', + 0x28 : 'ENABLE PIN', + 0x2A : 'PERFORM SECURITY OPERATION', + 0x2C : 'UNBLOCK PIN', + 0x32 : 'INCREASE', + 0x44 : 'ACTIVATE FILE', + 0x46 : 'GENERATE ASYMETRIC KEY PAIR', + 0x70 : 'MANAGE CHANNEL', + 0x73 : 'MANAGE SECURE CHANNEL', + 0x75 : 'TRANSACT DATA', + 0x84 : 'GET CHALLENGE', + 0x86 : 'GENERAL AUTHENTICATE', + 0x87 : 'GENERAL AUTHENTICATE', + 0x88 : 'INTERNAL AUTHENTICATE', + 0x89 : 'AUTHENTICATE', + 0xA0 : 'SEARCH BINARY', + 0xA1 : 'SEARCH BINARY', + 0xA2 : 'SEARCH RECORD', + 0xA4 : 'SELECT FILE', + 0xAA : 'TERMINAL CAPABILITY', + 0xB0 : 'READ BINARY', + 0xB1 : 'READ BINARY', + 0xB2 : 'READ RECORD(S)', + 0xB3 : 'READ RECORD(S)', + 0xC0 : 'GET RESPONSE', + 0xC2 : 'ENVELOPE', + 0xC3 : 'ENVELOPE', + 0xCA : 'RETRIEVE DATA', + 0xCB : 'RETRIEVE DATA', + 0xD2 : 'WRITE RECORD', + 0xD6 : 'UPDATE BINARY', + 0xD7 : 'UPDATE BINARY', + 0xDA : 'SET DATA', + 0xDB : 'SET DATA', + 0xDC : 'UPDATE RECORD', + 0xDD : 'UPDATE RECORD', + 0xE0 : 'CREATE FILE', + 0xE2 : 'APPEND RECORD', + 0xE4 : 'DELETE FILE', + 0xE6 : 'TERMINATE DF', + 0xE8 : 'TERMINATE EF', + 0xF2 : 'STATUS', + 0xFE : 'TERMINATE CARD USAGE', + } + + file_tags = { + 0x80 : 'Size', + 0x81 : 'Length', + 0x82 : 'File Descriptor', + 0x83 : 'File Identifier', + 0x84 : 'DF Name', + 0x85 : 'Proprietary no-BERTLV', + 0x86 : 'Proprietary Security Attribute', + 0x87 : 'EF with FCI extension', + 0x88 : 'Short File Identifier', + 0x8A : 'Life Cycle Status', + 0x8B : 'Security Attributes ref to expanded', + 0x8C : 'Security Attributes compact', + 0x8D : 'EF with Security Environment', + 0x8E : 'Channel Security Attribute', + 0xA0 : 'Security Attribute for DO', + 0xA1 : 'Proprietary Security Attribute', + 0xA2 : 'DO Pairs', + 0xA5 : 'Proprietary BERTLV', + 0xAB : 'Security Attribute expanded', + } + + def __init__(self, CLA=0x00): + ''' + connect smartcard and defines class CLA code for communication + uses "pyscard" library services + + creates self.CLA attribute with CLA code + and self.coms attribute with associated "apdu_stack" instance + ''' + cardtype = AnyCardType() + cardrequest = CardRequest(timeout=1, cardType=cardtype) + self.cardservice = cardrequest.waitforcard() + self.cardservice.connection.connect() + self.reader = self.cardservice.connection.getReader() + self.ATR = self.cardservice.connection.getATR() + + self.CLA = CLA + self.coms = apdu_stack() + + def disconnect(self): + ''' + disconnect smartcard: stops the session + uses "pyscard" library service + ''' + self.cardservice.connection.disconnect() + + def define_class(self, CLA=0x00): + ''' + define smartcard class attribute for APDU command + override CLA value defined in class initialization + ''' + self.CLA = CLA + + def ATR_scan(self, smlist_file="/usr/local/share/pcsc/smartcard_list.txt"): + ''' + print smartcard info retrieved from AnswerToReset + thanks to pyscard routine + + if pcsc_scan is installed, + use the signature file passed as argument for guessing the card + + check also the more complete "parseATR" tool + ''' + print '\nsmartcard reader: ', self.reader + if self.ATR != None: + print "\nsmart card ATR is: %s" % toHexString(self.ATR) + print 'ATR analysis: ' + print ATR(self.ATR).dump() + print '\nhistorical bytes: ', \ + toHexString(ATR(self.ATR).getHistoricalBytes()) + ATRcs = ATR(self.ATR).getChecksum() + if ATRcs : + print 'checksum: ', "0x%X" % ATRcs + else: + print 'no ATR checksum' + print "\nusing pcsc_scan ATR list file: %s" % smlist_file + if os.path.exists(smlist_file): + smlist = open(smlist_file).readlines() + ATRre = re.compile('(^3[BF]){1}.{1,}$') + ATRfinger = '' + j = 1 + for i in range(len(smlist)): + if ATRre.match(smlist[i]): + if re.compile(smlist[i][:len(smlist[i])-1]).\ + match(toHexString(self.ATR)): + while re.compile('\t.{1,}').match(smlist[i+j]): + ATRfinger += smlist[i+j][1:] + j += j + if ATRfinger == '' : + print "no ATR fingerprint found in file: %s" % smlist_file + else: + print "smartcard ATR fingerprint:\n%s" % ATRfinger + else: + print "%s file not found" % smlist_file + + def sw_status(self, sw1, sw2): + ''' + sw_status(sw1=int, sw2=int) -> string + + SW status bytes interpretation as defined in ISO-7816 part 4 standard + helps to speak and understand with the smartcard! + ''' + status = 'undefined status' + if sw1 == 0x90 and sw2 == 0x00: status = 'normal processing: ' \ + 'command accepted: no further qualification' + elif sw1 == 0x61: status = 'normal processing: %i bytes ' \ + 'still available' % sw2 + elif sw1 == 0x62: + status = 'warning processing: state of non-volatile '\ + 'memory unchanged' + if sw2 == 0x00: status += ': no information given' + elif sw2 == 0x81: status += ': part of returned data may' \ + 'be corrupted' + elif sw2 == 0x82: status += ': end of file/record reached ' \ + 'before reading Le bytes' + elif sw2 == 0x83: status += ': selected file invalidated' + elif sw2 == 0x84: status += ': FCI not formatted' + elif sw2 == 0x85: status += ': selected file in termination state' + elif sw2 == 0x86: status += ': no input data available ' \ + 'from a sensor on the card' + elif 0x01 < sw2 < 0x81: status += ': card has %s bytes pending' \ + % toHexString([sw2])[1] + else: status += ': undefined SW2 code: 0x%s' % toHexString([sw2]) + elif sw1 == 0x63: + status = 'warning processing: state of non-volatile memory changed' + if sw2 == 0x00: status += ': no information given' + elif sw2 == 0x81: status += ': file filled up by the last write' + elif 0xC0 <= sw2 <= 0xCF: status += ': counter provided by %s' \ + % toHexString([sw2])[1] + else: status += ': undefined SW2 code: 0x%s' % toHexString([sw2]) + elif sw1 == 0x64: + status = 'execution error: state of non-volatile memory unchanged' + if sw2 == 0x01: status += ': immediate response expected ' \ + 'by the card' + elif 0x01 < sw2 < 0x81: status += ': command aborted ' \ + 'by the card, recovery of %s bytes is needed' \ + % toHexString([sw2]) + else: status += ': undefined SW2 code: 0x%s' % toHexString([sw2]) + elif sw1 == 0x65: + status = 'execution error: state of non-volatile memory changed' + if sw2 == 0x00: status += ': no information given' + elif sw2 == 0x81: status += ': memory failure' + else: status += ': undefined SW2 code: 0x%s' % toHexString([sw2]) + elif sw1 == 0x66: status = 'execution error: reserved for ' \ + 'security-related issues' + elif sw1 == 0x67 and sw2 == 0x00: status = 'checking error: ' \ + 'wrong length (P3 parameter)' + elif sw1 == 0x68: + status = 'checking error: functions in CLA not supported' + if sw2 == 0x00: status += ': no information given' + elif sw2 == 0x81: status += ': logical channel not supported' + elif sw2 == 0x82: status += ': secure messaging not supported' + elif sw2 == 0x83: status += ': last command of the chain expected' + elif sw2 == 0x84: status += ': command chaining not supported' + else: status += ': undefined SW2 code: 0x%s' % toHexString([sw2]) + elif sw1 == 0x69: + status = 'checking error: command not allowed' + if sw2 == 0x00: status += ': no information given' + elif sw2 == 0x81: status += ': command incompatible with ' \ + 'file structure' + elif sw2 == 0x82: status += ': security status not satisfied' + elif sw2 == 0x83: status += ': authentication method blocked' + elif sw2 == 0x84: status += ': referenced data invalidated' + elif sw2 == 0x85: status += ': conditions of use not satisfied' + elif sw2 == 0x86: status += ': command not allowed (no current EF)' + elif sw2 == 0x87: status += ': expected SM data objects missing' + elif sw2 == 0x88: status += ': SM data objects incorrect' + else: status += ': undefined SW2 code: 0x%s' % toHexString([sw2]) + elif sw1 == 0x6A: + status = 'checking error: wrong parameter(s) P1-P2' + if sw2 == 0x00: status += ': no information given' + elif sw2 == 0x80: status += ': incorrect parameters ' \ + 'in the data field' + elif sw2 == 0x81: status += ': function not supported' + elif sw2 == 0x82: status += ': file not found' + elif sw2 == 0x83: status += ': record not found' + elif sw2 == 0x84: status += ': not enough memory space in the file' + elif sw2 == 0x85: status += ': Lc inconsistent with TLV structure' + elif sw2 == 0x86: status += ': incorrect parameters P1-P2' + elif sw2 == 0x87: status += ': Lc inconsistent with P1-P2' + elif sw2 == 0x88: status += ': referenced data not found' + elif sw2 == 0x89: status += ': file already exists' + elif sw2 == 0x8A: status += ': DF name already exists' + else: status += ': undefined SW2 code: 0x%s' % toHexString([sw2]) + elif sw1 == 0x6B and sw2 == 0x00: status = 'checking error: '\ + 'wrong parameter(s) P1-P2' + elif sw1 == 0x6C: status = 'checking error: wrong length Le: ' \ + 'exact length is %s' % toHexString([sw2]) + elif sw1 == 0x6D and sw2 == 0x00: status = 'checking error: ' \ + 'instruction code not supported or invalid' + elif sw1 == 0x6E and sw2 == 0x00: status = 'checking error: ' \ + 'class not supported' + elif sw1 == 0x6F and sw2 == 0x00: status = 'checking error: ' \ + 'no precise diagnosis' + return status + + def sr_apdu(self, apdu, force=False): + ''' + sr_apdu(apdu=[0x.., 0x.., ...]) -> + list [ string(apdu sent information), + string(SW codes interpretation), + 2-tuple(sw1, sw2), + list(response bytes) ] + + generic function to send apdu, receive and interpret response + force: force card reconnection if pyscard transmission fails + ''' + if force: + try: + data, sw1, sw2 = self.cardservice.connection.transmit(apdu) + except CardConnectionException: + ISO7816.__init__(self, CLA = self.CLA) + data, sw1, sw2 = self.cardservice.connection.transmit(apdu) + else: + data, sw1, sw2 = self.cardservice.connection.transmit(apdu) + # replaces INS code by strings when available + if apdu[1] in self.INS_dic.keys(): + apdu_name = self.INS_dic[apdu[1]] + ' ' + else: + apdu_name = '' + sw_stat = self.sw_status(sw1, sw2) + return ['%sapdu: %s' % (apdu_name, toHexString(apdu)), + 'sw1, sw2: %s - %s' % ( toHexString([sw1, sw2]), sw_stat ), + (sw1, sw2), + data ] + + def bf_cla(self, start=0, param=[0xA4, 0x00, 0x00, 0x02, 0x3F, 0x00]): + ''' + bf_cla( start=int(starting CLA), + param=list(bytes for selecting file 0x3F, 0x00) ) -> + list( CLA which could be supported ) + + tries all classes CLA codes to check the possibly supported ones + prints CLA suspected to be supported + returns the list of those CLA codes + + WARNING: + can block the card definitively + Do not do it with your own VISA / MASTERCARD + ''' + clist = [] + for i in range(start, 256): + ret = self.sr_apdu([i] + param) + if ret[2] != (0x6E, 0x00): + print ret + clist.append(i) + return clist + + def bf_ins(self, start=0): + ''' + bf_cla( start=int(starting INS) ) + -> list( INS which could be supported ) + + tries all instructions INS codes to check the supported ones + prints INS suspected to be supported + returns the list of those INS codes + + WARNING: + can block the card definitively + Do not do it with your own VISA / MASTERCARD + ''' + ilist = [] + for i in range(start, 256): + if self.dbg > 1: + print 'DEBUG: testing %d for INS code with %d CLA code' \ + % (i, self.CLA) + ret = self.sr_apdu([self.CLA, i, 0x00, 0x00]) + if ret[2] != (0x6D, 0x00): + print ret + ilist.append(i) + return ilist + + ### + # Below is defined a list of standard commands to be used with (U)SIM cards + # They are mainly defined and described in + # ISO 7816 and described further in ETSI 101.221 + ### + def READ_BINARY(self, P1=0x00, P2=0x00, Le=0x01): + ''' + APDU command to read the content of EF file with transparent structure + Le: length of data bytes to be read + + call sr_apdu method + ''' + READ_BINARY = [self.CLA, 0xB0, P1, P2, Le] + return self.sr_apdu(READ_BINARY) + + def WRITE_BINARY(self, P1=0x00, P2=0x00, Data=[]): + ''' + APDU command to write the content of EF file with transparent structure + + Data: list of data bytes to be written + call sr_apdu method + ''' + WRITE_BINARY = [self.CLA, 0xD0, P1, P2, len(Data)] + Data + return self.sr_apdu(WRITE_BINARY) + + def UPDATE_BINARY(self, P1=0x00, P2=0x00, Data=[]): + ''' + APDU command to update the content of EF file with transparent structure + + Data: list of data bytes to be written + call sr_apdu method + ''' + UPDATE_BINARY = [self.CLA, 0xD6, P1, P2, len(Data)] + Data + return self.sr_apdu(UPDATE_BINARY) + + def ERASE_BINARY(self, P1=0x00, P2=0x00, Lc=None, Data=[]): + ''' + APDU command to erase the content of EF file with transparent structure + + Lc: 'None' or '0x02' + Data: list of data bytes to be written + call sr_apdu method + ''' + if Lc is None: + ERASE_BINARY = [self.CLA, 0x0E, P1, P2] + else: + ERASE_BINARY = [self.CLA, 0x0E, P1, P2, 0x02] + Data + return self.sr_apdu(ERASE_BINARY) + + def READ_RECORD(self, P1=0x00, P2=0x00, Le=0x00): + ''' + APDU command to read the content of EF file with record structure + + P1: record number + P2: reference control + Le: length of data bytes to be read + call sr_apdu method + ''' + READ_RECORD = [self.CLA, 0xB2, P1, P2, Le] + return self.sr_apdu(READ_RECORD) + + def WRITE_RECORD(self, P1=0x00, P2=0x00, Data=[]): + ''' + APDU command to write the content of EF file with record structure + + P1: record number + P2: reference control + Data: list of data bytes to be written in the record + call sr_apdu method + ''' + WRITE_RECORD = [self.CLA, 0xD2, P1, P2, len(Data)] + Data + return self.sr_apdu(WRITE_RECORD) + + def APPEND_RECORD(self, P2=0x00, Data=[]): + ''' + APDU command to append a record on EF file with record structure + + P2: reference control + Data: list of data bytes to be appended on the record + call sr_apdu method + ''' + APPEND_RECORD = [self.CLA, 0xE2, 0x00, P2, len(Data)] + Data + return self.sr_apdu(APPEND_RECORD) + + def UPDATE_RECORD(self, P1=0x00, P2=0x00, Data=[]): + ''' + APDU command to update the content of EF file with record structure + + P1: record number + P2: reference control + Data: list of data bytes to update the record + call sr_apdu method + ''' + APPEND_RECORD = [self.CLA, 0xDC, P1, P2, len(Data)] + Data + return self.sr_apdu(APPEND_RECORD) + + def GET_DATA(self, P1=0x00, P2=0x00, Le=0x01): + ''' + APDU command to retrieve data object + + P1 and P2: reference control for data object description + Le: number of bytes expected in the response + call sr_apdu method + ''' + GET_DATA = [self.CLA, 0xCA, P1, P2, Le] + return self.sr_apdu(GET_DATA) + + def PUT_DATA(self, P1=0x00, P2=0x00, Data=[]): + ''' + APDU command to store data object + + P1 and P2: reference control for data object description + Data: list of data bytes to put in the data object structure + call sr_apdu method + ''' + if len(Data) == 0: + PUT_DATA = [self.CLA, 0xDA, P1, P2] + elif 1 <= len(Data) <= 255: + PUT_DATA = [self.CLA, 0xDA, P1, P2, len(Data)] + Data + # should never be the case, however... who wants to try + else: + PUT_DATA = [self.CLA, 0xDA, P1, P2, 0xFF] + Data[0:255] + return self.sr_apdu(PUT_DATA) + + def SELECT_FILE(self, P1=0x00, P2=0x00, Data=[0x3F, 0x00], \ + with_length=True): + ''' + APDU command to select file + + P1 and P2: selection control + Data: list of bytes describing the file identifier or address + call sr_apdu method + ''' + if with_length: + Data = [min(len(Data), 255)] + Data + SELECT_FILE = [self.CLA, 0xA4, P1, P2] + Data + return self.sr_apdu(SELECT_FILE) + + def VERIFY(self, P2=0x00, Data=[]): + ''' + APDU command to verify user PIN, password or security codes + + P2: reference control + Data: list of bytes to be verified by the card + call sr_apdu method + ''' + if len(Data) == 0: + VERIFY = [self.CLA, 0x20, 0x00, P2] + elif 1 <= len(Data) <= 255: + VERIFY = [self.CLA, 0x20, 0x00, P2, len(Data)] + Data + # should never be the case, however... who wants to try + else: + VERIFY = [self.CLA, 0x20, 0x00, P2, 0xFF] + Data[0:255] + return self.sr_apdu(VERIFY) + + def INTERNAL_AUTHENTICATE(self, P1=0x00, P2=0x00, Data=[]): + ''' + APDU command to run internal authentication algorithm + + P1 and P2: reference control (algo, secret key selection...) + Data: list of bytes containing the authentication challenge + call sr_apdu method + ''' + INTERNAL_AUTHENTICATE = [self.CLA, 0x88, P1, P2, len(Data)] + Data + return self.sr_apdu(INTERNAL_AUTHENTICATE) + + def EXTERNAL_AUTHENTICATE(self, P1=0x00, P2=0x00, Data=[]): + ''' + APDU command to conditionally update the security status of the card + after getting a challenge from it + + P1 and P2: reference control (algo, secret key selection...) + Data: list of bytes containing the challenge response + call sr_apdu method + ''' + if len(Data) == 0: + EXTERNAL_AUTHENTICATE = [self.CLA, 0x82, P1, P2] + elif 1 <= len(Data) <= 255: + EXTERNAL_AUTHENTICATE = [self.CLA, 0x82, P1, P2, len(Data)] + Data + # should never be the case, however... who wants to try + else: + EXTERNAL_AUTHENTICATE = [self.CLA, 0x82, P1, P2, 0xFF] + Data[0:255] + return self.sr_apdu(EXTERNAL_AUTHENTICATE) + + def GET_CHALLENGE(self): + ''' + APDU command to get a challenge for external entity authentication + to the card + + call sr_apdu method + ''' + GET_CHALLENGE = [self.CLA, 0x84, 0x00, 0x00] + return self.sr_apdu(GET_CHALLENGE) + + def MANAGE_CHANNEL(self, P1=0x00, P2=0x00): + ''' + APDU to open and close supplementary logical channels + + P1=0x00 to open, 0x80 to close + P2=0x00, 1, 2 or 3 to ask for logical channel number + call sr_apdu method + ''' + if (P1, P2) == (0x00, 0x00): + MANAGE_CHANNEL = [self.CLA, 0x70, P1, P2, 0x01] + else: + MANAGE_CHANNEL = [self.CLA, 0x70, P1, P2] + return self.sr_apdu(MANAGE_CHANNEL) + + def GET_RESPONSE(self, Le=0x01): + ''' + APDU command to retrieve data after selection + or other kind of request that should get an extensive reply + + Le: expected length of data + call sr_apdu method + ''' + GET_RESPONSE = [self.CLA, 0xC0, 0x00, 0x00, Le] + return self.sr_apdu(GET_RESPONSE) + + def ENVELOPPE(self, Data=[]): + ''' + APDU command to encapsulate data (APDU or other...) + check ETSI TS 102.221 for some examples... + + Data: list of bytes + call sr_apdu method + ''' + if len(Data) == 0: + ENVELOPPE = [self.CLA, 0xC2, 0x00, 0x00] + elif 1 <= len(Data) <= 255: + ENVELOPPE = [self.CLA, 0xC2, 0x00, 0x00, len(Data)] + Data + return self.sr_apdu(ENVELOPPE) + + def SEARCH_RECORD(self, P1=0x00, P2=0x00, Data=[]): + ''' + APDU command to seach pattern in the current EF file + with record structure + + P1: record number + P2: type of search + Data: list of bytes describing a pattern to search for + call sr_apdu method + ''' + SEARCH_RECORD = [self.CLA, 0xA2, P1, P2, len(Data)] + Data + return self.sr_apdu(SEARCH_RECORD) + + def DISABLE_CHV(self, P1=0x00, P2=0x00, Data=[]): + ''' + APDU command to disable CHV verification (such as PIN or password...) + + P1: let to 0x00... or read ISO and ETSI specifications + P2: type of CHV to disable + Data: list of bytes for CHV value + call sr_apdu method + ''' + DISABLE_CHV = [self.CLA, 0x26, P1, P2, len(Data)] + Data + return self.sr_apdu(DISABLE_CHV) + + def UNBLOCK_CHV(self, P2=0x00, Lc=None, Data=[]): + ''' + APDU command to unblock CHV code (e.g. with PUK for deblocking PIN) + + P2: type of CHV to unblock + Lc: Empty or 0x10 + Data: if Lc=0x10, UNBLOCK_CHV value and new CHV value to set + call sr_apdu method + + TODO: check the exact coding for the Data + ''' + if Lc is None: + UNBLOCK_CHV = [self.CLA, 0x2C, 0x00, P2] + else: + UNBLOCK_CHV = [self.CLA, 0x2C, 0x00, P2, 0x10] + Data + return self.sr_apdu(UNBLOCK_CHV) + + ########################## + # evolved "macro" method for ISO7816 card + # need the "coms" attribute being an apdu_stack() + ########################## + def parse_file(self, Data=[]): + ''' + parse_file(self, Data) -> Dict() + + parses a list of bytes returned when selecting a file + interprets the content of some informative bytes + for file structure and parsing method... + ''' + ber = BERTLV_parser( Data ) + if self.dbg > 1: + print '[DBG] BER structure:\n%s' % ber + if len(ber) > 1: + # TODO: implements recursive BER object parsing + print '[WNG] more than 1 BER object: %s' % ber + + # for FCP control structure, precise parsing is done + # this structure seems to be the most used for (U)SIM cards + if ber[0][0][2] == 0x2: + fil = self.parse_FCP( ber[0][2] ) + fil['Control'] = 'FCP' + return fil + + # for other control structure, DIY + fil = {} + if ber[0][0][2] == 0x4: + fil['Control'] = 'FMD' + if self.dbg: + print '[WNG] FMD file structure parsing not implemented' + elif ber[0][0][2] == 0xF: + fil['Control'] = 'FCI' + if self.dbg: + print '[WNG] FCI file structure parsing not implemented' + else: + fil['Control'] = ber[0][0] + if self.dbg: + print '[WNG] unknown file structure' + fil['Data'] = ber[0][2] + + return fil + + def parse_FCP(self, Data=[]): + ''' + parse_FCP(Data) -> Dict() + + parses a list of bytes returned when selecting a file + interprets the content of some informative bytes + for file structure and parsing method... + ''' + fil = {} + # loop on the Data bytes to parse TLV'style attributes + toProcess = Data + while len(toProcess) > 0: + # TODO: seemd full compliancy + # would require to work with the BERTLV parser... + [T, L, V] = first_TLV_parser(toProcess) + if self.dbg > 2: + if T in self.file_tags.keys(): + Tag = self.file_tags[T] + else: + Tag = T + print '[DBG] %s / %s: %s' % (T, Tag, V) + + # do extra processing here + # File ID, DF name, Short file id + if T in (0x83, 0x84, 0x88): + fil[self.file_tags[T]] = V + # Security Attributes compact format + elif T == 0x8C: + fil[self.file_tags[T]] = V + fil = self.parse_security_attribute_compact(V, fil) + # Security Attributes + elif T in (0x86, 0x8B, 0x8E, 0xA0, 0xA1, 0xAB): + fil[self.file_tags[T]] = V + # TODO: no concrete parsing at this time... + fil = self.parse_security_attribute(V, fil) + # file size or length + elif T in (0x80, 0x81): + fil[self.file_tags[T]] = sum( [ V[i] * pow(0x100, len(V)-i-1) \ + for i in range(len(V)) ] ) + # file descriptor, deducting file access, type and structure + elif T == 0x82: + assert( L in (2, 5) ) + fil[self.file_tags[T]] = V + fil = self.parse_file_descriptor(V, fil) + # life cycle status + elif T == 0x8A: + fil = self.parse_life_cycle(V, fil) + # proprietary information + elif T == 0xA5: + fil = self.parse_proprietary(V, fil) + else: + if T in self.file_tags.keys(): + fil[self.file_tags[T]] = V + else: + fil[T] = V + + # truncate the data to process and loop + if L < 256: + toProcess = toProcess[L+2:] + else: + toProcess = toProcess[L+4:] + + # and return the file + return fil + + @staticmethod + def parse_life_cycle(Data, fil): + ''' + parses a list of bytes provided in Data + interprets the content as the life cycle + and enriches the file dictionnary passed as argument + ''' + if Data[0] == 1: fil['Life Cycle Status'] = 'creation state' + elif Data[0] == 3: fil['Life Cycle Status'] = 'initialization state' + elif Data[0] in (5, 7): fil['Life Cycle Status'] = 'operational state' \ + ' - activated' + elif Data[0] in (4, 6): fil['Life Cycle Status'] = 'operational state' \ + ' - deactivated' + elif Data[0] in range(12, 15): fil['Life Cycle Status'] = \ + 'termination state' + elif Data[0] >= 16: fil['Life Cycle Status'] = 'proprietary' + else: fil['Life Cycle Status'] = 'RFU' + return fil + + @staticmethod + def parse_file_descriptor(Data, fil): + ''' + parses a list of bytes provided in Data + interprets the content as the file descriptor + and enriches the file dictionnary passed as argument + ''' + # parse the File Descriptor Byte + fd = Data[0] + fd_type = (fd >> 3) & 0b00111 + fd_struct = fd & 0b00000111 + # get Structure, Access and Type + # bit b8 + if (fd >> 7) & 0b1: fil['Structure'] = 'RFU' + # access bit b7 + if (fd >> 6) & 0b1: fil['Access'] = 'shareable' + else : fil['Access'] = 'not shareable' + # structure bits b1 to b3 + if fd_struct == 0: fil['Structure'] = 'no information' + elif fd_struct == 1: fil['Structure'] = 'transparent' + elif fd_struct == 2: fil['Structure'] = 'linear fixed' + elif fd_struct == 3: fil['Structure'] = 'linear fixed TLV' + elif fd_struct == 4: fil['Structure'] = 'linear variable' + elif fd_struct == 5: fil['Structure'] = 'linear variable TLV' + elif fd_struct == 6: fil['Structure'] = 'cyclic' + elif fd_struct == 7: fil['Structure'] = 'cyclic TLV' + else : fil['Structure'] = 'RFU' + # type bits b4 to b6 + if fd_type == 0: fil['Type'] = 'EF working' + elif fd_type == 1: fil['Type'] = 'EF internal' + elif fd_type == 7: + fil['Type'] = 'DF' + if fd_struct == 1: fil['Structure'] = 'BER-TLV' + elif fd_struct == 2: fil['Structure'] = 'TLV' + else: fil['Type'] = 'EF proprietary' + + # for linear and cyclic EF: + # the following is convenient for UICC, + # but looks not fully conform to ISO standard + # see coding convention in ISO 7816-4 Table 87 + if len(Data) == 5: + fil['Record Length'], fil['Record Number'] = Data[3], Data[4] + + return fil + + @staticmethod + def parse_proprietary(Data, fil): + ''' + parses a list of bytes provided in Data + interprets the content as the proprietary parameters + and enriches the file dictionnary passed as argument + ''' + propr_tags = { + 0x80:"UICC characteristics", + 0x81:"Application power consumption", + 0x82:"Minimum application clock frequency", + 0x83:"Amount of available memory", + 0x84:"File details", + 0x85:"Reserved file size", + 0x86:"Maximum file size", + 0x87:"Supported system commands", + 0x88:"Specific UICC environmental conditions", + } + while len(Data) > 0: + [T, L, V] = first_TLV_parser( Data ) + if T in propr_tags.keys(): + fil[propr_tags[T]] = V + Data = Data[L+2:] + return fil + + @staticmethod + def parse_security_attribute_compact(Data, fil): + ''' + parses a list of bytes provided in Data + interprets the content as the compact form for security parameters + and enriches the file dictionnary passed as argument + ''' + # See ISO-IEC 7816-4 section 5.4.3, with compact and expanded format + AM = Data[0] + SC = Data[1:] + sec = '#' + + if 'Type' in fil.keys(): + # DF parsing + if fil['Type'] == 'DF': + if AM & 0b10000000 == 0: + if AM & 0b01000000: sec += ' DELETE FILE #' + if AM & 0b00100000: sec += ' TERMINATE DF #' + if AM & 0b00010000: sec += ' ACTIVATE FILE #' + if AM & 0b00001000: sec += ' DEACTIVATE FILE #' + if AM & 0b00000100: sec += ' CREATE DF #' + if AM & 0b00000010: sec += ' CREATE EF #' + if AM & 0b00000001: sec += ' DELETE FILE #' + # EF parsing + else: + if AM & 0b10000000 == 0: + if AM & 0b01000000: sec += ' DELETE FILE #' + if AM & 0b00100000: sec += ' TERMINATE EF #' + if AM & 0b00010000: sec += ' ACTIVATE FILE #' + if AM & 0b00001000: sec += ' DEACTIVATE FILE #' + if AM & 0b00000100: sec += ' WRITE / APPEND #' + if AM & 0b00000010: sec += ' UPDATE / ERASE #' + if AM & 0b00000001: sec += ' READ / SEARCH #' + + # loop on SC: + for cond in SC: + if cond == 0 : sec += ' Always #' + elif cond == 0xff: sec += ' Never #' + else: + sec += ' SEID %s #' % (cond & 0b00001111) + if cond & 0b10000000: sec += ' all conditions #' + else: sec += ' at least 1 condition #' + if cond & 0b01000000: sec += ' secure messaging #' + if cond & 0b00100000: sec += ' external authentication #' + if cond & 0b00010000: sec += ' user authentication #' + + #file['Security Attributes raw'] = Data + fil['Security Attributes'] = sec + return fil + + @staticmethod + def parse_security_attribute(Data, fil): + ''' + TODO: to implement... + + need to work further on how to do it (with ref to EF_ARR) + ''' + # See ISO-IEC 7816-4 section 5.4.3, with compact and expanded format + #if self.dbg: + # print '[DBG] parse_security_attribute() not implemented' + return fil + + def read_EF(self, fil): + ''' + interprets the content of file parameters (Structure, Size, Length...) + and enriches the file dictionnary passed as argument + with "Data" key and corresponding + - list of bytes for EF transparent + - list of list of bytes for cyclic or linear EF + ''' + # read EF transparent data + if fil['Structure'] == 'transparent': + self.coms.push( self.READ_BINARY(Le=fil['Size']) ) + if self.coms()[2] != (0x90, 0x00): + if self.dbg > 1: + print '[DBG] %s' % self.coms() + return fil + fil['Data'] = self.coms()[3] + + # read EF cyclic / linear all records data + elif fil['Structure'] != 'transparent': + fil['Data'] = [] + # for record data: need to check the number of recordings + # stored in the file, and iterate for each + for i in range( (fil['Size'] / fil['Record Length']) ): + self.coms.push( self.READ_RECORD(P1=i+1, P2=0x04, \ + Le=fil['Record Length']) ) + if self.coms()[2] != (0x90, 0x00): + # should mean there is an issue + # somewhere in the file parsing process + if self.dbg: + print '[WNG] error in iterating the RECORD parsing at' \ + ' iteration %s\n%s' % (i, self.coms()) + return fil + if self.coms()[3][1:] == len(self.coms()[3][1:]) * [255]: + # record is empty, contains padding only + pass + else: + fil['Data'].append(self.coms()[3]) + + # return the [Data] for transparent or + # [[Record1],[Record2]...] for cyclic / linear + return fil + + def select(self, Data=[0x3F, 0x00], typ="fid", with_length=True): + ''' + self.select(Data=[0x.., 0x..], typ="fid", with_length=True) + -> dict(file) on success, None on error + + selects the file + if error, returns None + if processing correct: gets response with info on the file + if processing correct and EF file: reads the data in the file + works in USIM fashion + else returns the data dictionnary: check parse_file_(U)SIM methods + last apdu available from the attribute self.coms + + different types of file selection are possible: + "fid": select by file id, only the direct child or + parent files of the last selected MF / DF / ADF + "pmf": select by path from MF + "pdf": select by path from last selected MF / DF / ADF + (or relative path) + "aid": select by ADF (Application) name + ''' + # get the UICC trigger + is_UICC = isinstance(self, UICC) + + # handle type of selection: + if typ == "pmf": P1 = 0x08 + elif typ == "pdf": P1 = 0x09 + elif typ == "aid": P1 = 0x04 + # the case of selection by "fid": + else: P1 = 0x00 + + # for UICC instance + # ask the return of the FCP template for the selected file: + if is_UICC: + P2 = 0x04 + else: + P2 = 0x00 + + # used to get back to MF without getting MF attributes: + if len(Data) == 0: + P1, P2 = 0x00, 0x0C + + # select file and check SW; if error, returns None, else get response + self.coms.push(self.SELECT_FILE(P1=P1, P2=P2, Data=Data, \ + with_length=with_length)) + + # different SW codes for UICC and old ISO card (e.g. SIM) + if is_UICC and self.coms()[2][0] != 0x61 \ + or not is_UICC and self.coms()[2][0] != 0x9F: + if self.dbg > 1: + print '[DBG] %s' % self.coms() + return None + + # get response and check SW: + # if error, return None, else parse file info + self.coms.push(self.GET_RESPONSE(Le=self.coms()[2][1])) + if self.coms()[2] != (0x90, 0x00): + if self.dbg > 1: + print '[DBG] %s' % self.coms() + return None + + data = self.coms()[3] + # take the `parse_file()' method from the instance: + # ISO7816, UICC or SIM + fil = self.parse_file(data) + if fil['Type'][0:2] == 'EF': + fil = self.read_EF(fil) + + # finally returns the whole file dictionnary, + # containing the ['Data'] key for EF file + return fil + + # + ############### + # TODO: + # improve all of the following... + ############### + + def flat_files_bf(self, path=[], under_AID=0, \ + hi_addr=(0, 0xff), lo_addr=(0, 0xff)): + ''' + flat_files_bf(self, path=[], under_AID=0, \ + hi_addr=(0, 0xff), lo_addr=(0, 0xff)) + -> list(files), list(DF_to_explore) + + path: path of the DF under MF or AID to brute force + under_AID: if > 0, select the AID number to init the brute force + only available for UICC instance + hi_addr: 8 MSB of the file address to brute force + lo_addr: 8 LSB of the file address to brute force + with_select_length: use the length parameter with SELECT instruction + + brute force file addresses of direct child under a given DF + get information on existing files, and discovered DF + + WARNING: not very tested yet... + ''' + # init return variables + FS, DF_to_explore = [], [] + # init selection process + MF, sel_type = [0x3F, 0x00], 'fid' + if isinstance(self, UICC): + sel_type = 'pdf' + + # start by selecting MF + try: + r = self.select(MF) + except: + print '[ERR] selecting MF failed' + return + if r == None: + print '[ERR] MF not found!' + return + + #if needed, select AID + if isinstance(self, UICC) and under_AID: + try: + self.get_AID() + r = self.select_by_aid(under_AID) + except: + print '[ERR] selecting AID failed' + return + if r == None: + print '[ERR] AID not found' + return + + # place on the DF path to bf + # select it by 'path from last selected DF' + if len(path) > 0: + try: + path_init = self.select(path, sel_type) + except: + print '[ERR] selecting path failed:\n%s' % self.coms() + return + if path_init == None: + print '[ERR] path not found: %s' % path + return + + # Dany'style programming + def reinit(): + self.select(MF) + if isinstance(self, UICC) and under_AID: + self.select_by_aid(under_AID) + if len(path) > 0: + self.select(path, sel_type) + + # loop over the address space to brute force files + i, j = 0, 0 + for i in range(hi_addr[0], hi_addr[1]): + if self.dbg and i%2 == 0: + print '[DBG] addr: %s %s %s' % ([hex(v) for v in path], \ + hex(i), hex(j)) + for j in range(lo_addr[0], lo_addr[1]): + # avoid MF re-selection: + if (i, j) == [0x3F, 0x00]: + fil = None + # select by direct file id + else: + fil = self.select([i, j], sel_type) + if fil is not None: + if self.dbg: + print '[DBG] found file at path, id: ' \ + '%s %s' % (path, [i, j]) + if 'File Identifier' in fil.keys(): + fil['Absolut Path'] = path+fil['File Identifier'] + FS.append(fil) + if 'Type' in fil.keys() and fil['Type'] == 'DF': + reinit() + if 'Absolut Path' in fil.keys(): + DF_to_explore.append(fil['Absolut Path']) + + # re-initialize at MF and return + self.select(MF) + return FS, DF_to_explore + + def init_FS(self): + self.FS = [] + + def recu_files_bf(self, path=[], under_AID=0): + ''' + recu_files_bf(self, path=[], under_AID=0) + -> void + + fills self.FS attribute with all files and DF discovered + recursively + ''' + # list all files and DF on the path + ret = self.flat_files_bf(path=path, under_AID=under_AID) + try: + self.FS += ret[0] + except: + '[ERR] FS not initialized: %s' % type(self.FS) + return + DF = ret[1] + + # recursive method call + # DF contains absolut path + for addr in DF: + print '[DBG] path: %s' % addr + self.recu_files_bf(path=addr, under_AID=under_AID) + + @staticmethod + def __write_dict(dict, fd): + keys = dict.keys() + keys.sort() + fd.write('\n') + for k in keys: + fd.write('%s: %s\n' % (k, dict[k])) + + + def scan_fs(self, filename='card_fs', stdout=False): + ''' + bf_files_under_MF(self, output='card_fs', stdout=True) + -> void + + filename: file to write found information in + stdout: print information on stdout too + + brute force all file addresses from MF and found AID + recursively (until no more DF are found) + write information on existing file on the output, + + WARNING: not very tested either... + ''' + fd = open(filename, 'w') + + self.init_FS() + self.recu_files_bf() + fd.write('\n### MF ###\n') + for f in self.FS: + self.__write_dict(f, fd) + fd.write('\n') + + # TODO: loop that + #self.init_FS() + #self.recu_files_bf(under_AID=1) + #fd.write('\n### AID #1 ###\n') + #for f in self.FS: + # self.__write_dict(f, fd) + # fd.write('\n') + + fd.close() +# + +############################################## +# UICC is defined in ETSI 102.221 mainly, and used for many telco applications +############################################## + +class UICC(ISO7816): + ''' + define attributes, methods and facilities for ETSI UICC card + check UICC specifications mainly in ETSI TS 102.221 + + inherits (eventually overrides) methods and objects from ISO7816 class + use self.dbg = 1 or more to print live debugging information + ''' + AID_RID = { + (0xA0, 0x00, 0x00, 0x00, 0x09): 'ETSI', + (0xA0, 0x00, 0x00, 0x00, 0x87): '3GPP', + (0xA0, 0x00, 0x00, 0x03, 0x43): '3GPP2', + (0xA0, 0x00, 0x00, 0x04, 0x12): 'OMA', + (0xA0, 0x00, 0x00, 0x04, 0x24): 'WiMAX', + } + ETSI_AID_app_code = { + (0x00, 0x00): 'Reserved', + (0x00, 0x01): 'GSM', + (0x00, 0x02): 'GSM SIM Toolkit', + (0x00, 0x03): 'GSM SIM API for JavaCard', + (0x00, 0x04): 'Tetra', + (0x00, 0x05): 'UICC API for JavaCard', + (0x01, 0x01): 'DVB CBMS KMS', + } + GPP_AID_app_code = { + (0x10, 0x01): 'UICC', + (0x10, 0x02): 'USIM', + (0x10, 0x03): 'USIM Toolkit', + (0x10, 0x04): 'ISIM', + (0x10, 0x05): 'USIM API for JavaCard', + (0x10, 0x06): 'ISIM API for JavaCard', + (0x10, 0x05): 'Contact Manager API for JavaCard', + } + GPP2_AID_app_code = { + (0x10, 0x02): 'CSIM', + } + AID_country_code = { + (0xFF, 0x33): 'France', + (0xFF, 0x44): 'United Kingdom', + (0xFF, 0x49): 'Germany', + } + + pin_status = { + 0x01 : "PIN Appl 1", + 0x02 : "PIN Appl 2", + 0x03 : "PIN Appl 3", + 0x04 : "PIN Appl 4", + 0x05 : "PIN Appl 5", + 0x06 : "PIN Appl 6", + 0x07 : "PIN Appl 7", + 0x08 : "PIN Appl 8", + 0x09 : "RFU", + 0x0A : "ADM1", + 0x0B : "ADM2", + 0x0C : "ADM3", + 0x0D : "ADM4", + 0x0E : "ADM5", + 0x11 : "PIN Universal PIN", + 0x81 : "Second PIN Appl 1", + 0x82 : "Second PIN Appl 2", + 0x83 : "Second PIN Appl 3", + 0x84 : "Second PIN Appl 4", + 0x85 : "Second PIN Appl 5", + 0x86 : "Second PIN Appl 6", + 0x87 : "Second PIN Appl 7", + 0x88 : "Second PIN Appl 8", + 0x89 : "RFU", + 0x8A : "ADM6", + 0x8B : "ADM7", + 0x8C : "ADM8", + 0x8D : "ADM9", + 0x8E : "ADM10", + } + + files = [ + ([0x3F, 0x00], 'MF', 'MF'), + ([0x2F, 0x00], 'EF', 'EF_DIR'), + ([0x2F, 0x01], 'EF', 'EF_ATR'), + ([0x2F, 0x05], 'EF', 'EF_PL'), + ([0x2F, 0x06], 'EF', 'EF_ARR'), + ([0x2F, 0x2E], 'EF', 'EF_ICCID'), + ([0x7F, 0xFF], 'DF', 'current ADF'), + ([0x7F, 0x10], 'DF', 'DF_TELECOM'), + ([0x7F, 0x10, 0x5F, 0x50], 'DF', 'DF_GRAPHICS'), + ([0x7F, 0x10, 0x5F, 0x3A], 'DF', 'DF_PHONEBOOK'), + ([0x7F, 0x20], 'DF', 'DF_GSM'), + ([0x7F, 0x21], 'DF', 'DF_DCS1800'), + ([0x7F, 0x22], 'DF', 'DF_IS-41'), + ([0x7F, 0x23], 'DF', 'DF_FP-CTS'), + ([0x7F, 0x24], 'DF', 'DF_TIA-EIA136'), + ([0x7F, 0x25], 'DF', 'DF_TIA-EIA95'), + ([0x7F, 0x80], 'DF', 'DF_PDC'), + ([0x7F, 0x90], 'DF', 'DF_TETRA'), + ([0x7F, 0x31], 'DF', 'DF_iDEN'), + ] + + def __init__(self): + ''' + initializes like an ISO7816-4 card with CLA=0x00 + and check available AID (Application ID) read from EF_DIR + + initializes on the MF + ''' + 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) + #print '[DBG] EF_DIR file selection and reading...' + + def parse_file(self, Data=[]): + ''' + parse_file(Data=[0x12, 0x34, 0x56, 0x89]) -> dict(file) + mainly based on the ISO7816 parsing style + + parses a list of bytes returned when selecting a file + interprets the content of some informative bytes for right accesses, + type / format of file... see TS 102.221 + works over the UICC file structure (quite different from e.g. SIM card) + ''' + # First ISO7816 parsing + fil = ISO7816.parse_file(self, Data) + + # Then UICC extra attributes parsing + if 0xC6 in fil.keys(): + fil = self.parse_pin_status(fil[0xC6], fil) + del fil[0xC6] + + if 'File Identifier' in fil.keys(): + for ref in self.files: + if fil['File Identifier'] == ref[0]: + fil['Name'] = ref[2] + + # return the enriched file + return fil + + @staticmethod + def parse_pin_status(Data, fil): + ''' + parses a list of bytes provided in Data + interprets the content as the UICC pin status + and enriches the file dictionnary passed as argument + ''' + PS_DO = Data[2:2+Data[1]] + Data = Data[2+len(PS_DO):] + PIN_status = '' + while len(Data) > 0: + [T, L, V] = first_TLV_parser(Data) + assert( T in (0x83, 0x95) ) + if T == 0x95: # PIN usage + if (V[0] << 7) & 1: + PIN_status += '#use verification / encipherment ' \ + '/ external authentication: ' + elif (V[0] << 6) & 1: + PIN_status += '#use computation / decipherment ' \ + '/ internal authentication: ' + elif (V[0] << 5) & 1: + PIN_status += '#use SM response: ' + elif (V[0] << 4) & 1: + PIN_status += '#use SM command: ' + elif (V[0] << 3) & 1: + PIN_status += '#use PIN verification: ' + elif (V[0] << 3) & 1: + PIN_status += '#use biometric user verification: ' + elif V[0] == 0: + PIN_status += '#verification not required: ' + elif T == 0x83: # PIN status + if len(PIN_status) == 0: PIN_status = '#' + if 0x00 < V[0] < 0x12 or 0x81 <= V[0] < 0x90: + PIN_status += UICC.pin_status[V[0]] + '#' + elif 0x12 <= V[0] < 0x1E: + PIN_status += 'RFU (Global)#' + elif 0x90 <= V[0] < 0x9F: + PIN_status += 'RFU (Local)#' + else: + PIN_status += '#' + #if self.dbg >= 2: + # print '[DBG] %s: %s; PIN status: %s' % (T, V, PIN_status) + Data = Data[L+2:] + fil['PIN Status'] = PIN_status + return fil + + def get_AID(self): + ''' + checks EF_DIR at the MF level, + and available AID (Application ID) referenced + + puts it into self.AID + interprets and print the content of the self.AID list + ''' + #go back to MF and select EF_DIR + #self.select(Data=[]) + + # EF_DIR is at the MF level and contains Application ID: + EF_DIR = self.select([0x2F, 0x00], typ='pmf') + if self.dbg: + print '[DBG] EF_DIR: %s' % EF_DIR + if EF_DIR is None: + return None + + # EF_DIR is an EF with linear fixed structure: contains records: + for rec in EF_DIR['Data']: + # check for a (new) AID: + if (rec[0], rec[2]) == (0x61, 0x4F) and len(rec) > 6 \ + and rec[4:4+rec[3]] not in self.AID: + self.AID.append( rec[4:4+rec[3]] ) + + i = 1 + for aid in self.AID: + aid_rid = tuple(aid[0:5]) + aid_app = tuple(aid[5:7]) + aid_country = tuple(aid[7:9]) + aid_provider = tuple(aid[9:11]) + + # get AID application code, depending on SDO... + if aid_rid == (0xA0, 0x00, 0x00, 0x00, 0x09) \ + and aid_app in self.ETSI_AID_app_code.keys(): + aid_app = self.ETSI_AID_app_code[aid_app] + if aid_rid == (0xA0, 0x00, 0x00, 0x00, 0x87) \ + and aid_app in self.GPP_AID_app_code.keys(): + aid_app = self.GPP_AID_app_code[aid_app] + if aid_rid == (0xA0, 0x00, 0x00, 0x03, 0x43) \ + and aid_app in self.GPP2_AID_app_code.keys(): + aid_app = self.GPP2_AID_app_code[aid_app] + # get AID responsible SDO and country + if aid_rid in self.AID_RID.keys(): aid_rid = self.AID_RID[aid_rid] + if aid_country in self.AID_country_code.keys(): + aid_country = self.AID_country_code[aid_country] + + print 'found [AID %s] %s || %s || %s || %s || %s' \ + % (i, aid_rid, aid_app, aid_country, \ + aid_provider, tuple(aid[11:]) ) + i += 1 + + def get_ICCID(self): + ''' + check EF_ICCID at the MF level, + and returnq the ASCII value of the ICCID + ''' + #go back to MF and select EF_ICCID + #self.select(Data=[]) + + # EF_ICCID is at the MF level and contains Application ID: + EF_ICCID = self.select([0x2F, 0xE2], typ='pmf') + if self.dbg: + print '[DBG] EF_ICCID: %s' % EF_ICCID + if EF_ICCID is None: + return None + return decode_BCD( EF_ICCID['Data'] ) + + def select_by_name(self, name=''): + ''' + AID selection by name taken from UICC.files + ''' + for i in range(len(self.files)): + if name == self.files[i][2]: + return self.select( self.files[i][0], 'pmf' ) + + def select_by_aid(self, aid_num=1): + ''' + AID selection by index + ''' + if len(self.AID) != 0: + return self.select(self.AID[aid_num-1], 'aid') + + + diff --git a/card/SIM.py b/card/SIM.py new file mode 100644 index 0000000..1ad45e3 --- /dev/null +++ b/card/SIM.py @@ -0,0 +1,274 @@ +""" +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 +# SIM card +# communication based on ISO7816 card +# +# needs pyscard from: +# http://pyscard.sourceforge.net/ +################################# + +from card.ICC import ISO7816 +from card.FS import SIM_FS +from card.utils import * + + +class SIM(ISO7816): + ''' + define attributes, methods and facilities for ETSI / 3GPP SIM card + check SIM specifications in ETSI TS 102.221 and 3GPP TS 51.011 + + inherit methods and objects from ISO7816 class + use self.dbg = 1 or more to print live debugging information + ''' + + def __init__(self): + ''' + initialize like an ISO7816-4 card with CLA=0xA0 + can also be used for USIM working in SIM mode, + ''' + ISO7816.__init__(self, CLA=0xA0) + if self.dbg: + print '[DBG] type definition: %s' % type(self) + print '[DBG] CLA definition: %s' % hex(self.CLA) + + + def sw_status(self, sw1, sw2): + ''' + sw_status(sw1=int, sw2=int) -> string + + extends SW status bytes interpretation from ISO7816 + with ETSI / 3GPP SW codes + helps to speak with the smartcard! + ''' + status = ISO7816.sw_status(self, sw1, sw2) + if sw1 == 0x91: status = 'normal processing, with extra info ' \ + 'containing a command for the terminal: length of the ' \ + 'response data %d' % sw2 + elif sw1 == 0x9E: status = 'normal processing, SIM data download ' \ + 'error: length of the response data %d' % sw2 + elif sw1 == 0x9F: status = 'normal processing: length of the ' \ + 'response data %d' % sw2 + elif (sw1, sw2) == (0x93, 0x00): status = 'SIM application toolkit ' \ + 'busy, command cannot be executed at present' + elif sw1 == 0x92 : + status = 'memory management' + if sw2 < 16: status += ': command successful but after %d '\ + 'retry routine' % sw2 + elif sw2 == 0x40: status += ': memory problem' + elif sw1 == 0x94: + status = 'referencing management' + if sw2 == 0x00: status += ': no EF selected' + elif sw2 == 0x02: status += ': out of range (invalid address)' + elif sw2 == 0x04: status += ': file ID or pattern not found' + elif sw2 == 0x08: status += ': file inconsistent with the command' + elif sw1 == 0x98: + status = 'security management' + if sw2 == 0x02: status += ': no CHV initialized' + elif sw2 == 0x04: status += ': access condition not fulfilled, ' \ + 'at least 1 attempt left' + elif sw2 == 0x08: status += ': in contradiction with CHV status' + elif sw2 == 0x10: status += ': in contradiction with ' \ + 'invalidation status' + elif sw2 == 0x40: status += ': unsuccessful CHV verification, ' \ + 'no attempt left' + elif sw2 == 0x50: status += ': increase cannot be performed, ' \ + 'max value reached' + elif sw2 == 0x62: status += ': authentication error, ' \ + 'application specific' + elif sw2 == 0x63: status += ': security session expired' + return status + + def verify_pin(self, pin='', pin_type=1): + ''' + verify CHV1 (PIN code) or CHV2 with VERIFY APDU command + call ISO7816 VERIFY method + ''' + if pin_type in [1, 2] and type(pin) is str and \ + len(pin) == 4 and 0 <= int(pin) < 10000: + PIN = [ord(i) for i in pin] + [0xFF, 0xFF, 0xFF, 0xFF] + self.coms.push( self.VERIFY(P2=pin_type, Data=PIN) ) + else: + if self.dbg: + print '[WNG] bad parameters' + + def disable_pin(self, pin='', pin_type=1): + ''' + disable CHV1 (PIN code) or CHV2 with DISABLE_CHV APDU command + TIP: do it as soon as you can when you are working + with a SIM / USIM card for which you know the PIN! + call ISO7816 DISABLE method + ''' + if pin_type in [1, 2] and type(pin) is str and \ + len(pin) == 4 and 0 <= int(pin) < 10000: + PIN = [ord(i) for i in pin] + [0xFF, 0xFF, 0xFF, 0xFF] + self.coms.push( self.DISABLE_CHV(P2=pin_type, Data=PIN) ) + else: + if self.dbg: + print '[WNG] bad parameters' + + def unblock_pin(self, pin_type=1, unblock_pin=''): + ''' + WARNING: not correctly implemented!!! + and PUK are in general 8 nums... + TODO: make it correctly! + + unblock CHV1 (PIN code) or CHV2 with UNBLOCK_CHV APDU command + and set 0000 value for new PIN + call ISO7816 UNBLOCK_CHV method + ''' + print 'not correctly implemented' + return + #if pin_type == 1: + # pin_type = 0 + if pin_type in [0, 2] and type(unblock_pin) is str and \ + len(unblock_pin) == 4 and 0 <= int(unblock_pin) < 10000: + UNBL_PIN = [ord(i) for i in unblock_pin] + [0xFF, 0xFF, 0xFF, 0xFF] + self.coms.push( self.UNBLOCK_CHV(P2=pin_type, Lc=0x10, \ + Data=UNBL_PIN + \ + [0x30, 0x30, 0x30, 0x30, 0xFF, 0xFF, 0xFF, 0xFF]) ) + else: + if self.dbg: + print '[WNG] bad parameters' + #return self.UNBLOCK_CHV(P2=pin_type) + + def parse_file(self, Data=[]): + ''' + parse_file(Data=[0x12, 0x34, 0x56, 0x89]) -> dict(file) + + parses a list of bytes returned when selecting a file + interprets the content of some informative bytes for right accesses, + type / format of file... see TS 51.011 + works over the SIM file structure + ''' + fil = {} + fil['Size'] = Data[2]*0x100 + Data[3] + fil['File Identifier'] = Data[4:6] + fil['Type'] = ('RFU', 'MF', 'DF', '', 'EF')[Data[6]] + fil['Length'] = Data[12] + if fil['Type'] == 'MF' or fil['Type'] == 'DF': + fil['DF_num'] = Data[14] + fil['EF_num'] = Data[15] + fil['codes_num'] = Data[16] + fil['CHV1'] = ('not initialized','initialized')\ + [(Data[18] & 0x80) / 0x80]\ + + ': %d attempts remain' % (Data[18] & 0x0F) + fil['unblock_CHV1'] = ('not initialized','initialized')\ + [(Data[19] & 0x80) / 0x80]\ + + ': %d attempts remain' % (Data[19] & 0x0F) + fil['CHV2'] = ('not initialized','initialized')\ + [(Data[20] & 0x80) / 0x80]\ + + ': %d attempts remain' % (Data[20] & 0x0F) + fil['unblock_CHV2'] = ('not initialized','initialized')\ + [(Data[21] & 0x80) / 0x80]\ + + ': %d attempts remain' % (Data[21] & 0x0F) + if len(Data) > 23: + fil['Adm'] = Data[23:] + elif fil['Type'] == 'EF': + cond = ('ALW', 'CHV1', 'CHV2', 'RFU', 'ADM_4', 'ADM_5', + 'ADM_6', 'ADM_7', 'ADM_8', 'ADM_9', 'ADM_A', + 'ADM_B', 'ADM_C', 'ADM_D', 'ADM_E', 'NEW') + fil['UPDATE'] = cond[Data[8] & 0x0F] + fil['READ'] = cond[Data[8] >> 4] + fil['INCREASE'] = cond[Data[9] >> 4] + fil['INVALIDATE'] = cond[Data[10] & 0x0F] + fil['REHABILITATE'] = cond[Data[10] >> 4] + fil['Status'] = ('not read/updatable when invalidated', + 'read/updatable when invalidated')\ + [byteToBit(Data[11])[5]] \ + + (': invalidated',': not invalidated')\ + [byteToBit(Data[11])[7]] + fil['Structure'] = ('transparent', 'linear fixed', '', 'cyclic')\ + [Data[13]] + if fil['Structure'] == 'cyclic': + fil['INCREASE'] = byteToBit(Data[7])[1] + if len(Data) > 14: + fil['Record Length'] = Data[14] + return fil + + def run_gsm_alg(self, RAND=16*[0x00]): + ''' + self.run_gsm_alg( RAND ) -> ( SRES, Kc ) + RAND : list of bytes, length 16 + SRES : list of bytes, length 4 + Kc : list of bytes, length 8 + + run GSM authentication algorithm: + accepts any kind of RAND (old GSM fashion) + feed with RAND 16 bytes value + return a list with SRES and Kc, or None on error + ''' + if len(RAND) != 16: + if self.dbg: + print '[WNG] needs a 16 bytes input RAND value' + return None + # select DF_GSM directory + self.select([0x7F, 0x20]) + if self.coms()[2] != (0x90, 0x00): + if self.dbg: + print '[DBG] %s' % self.coms() + return None + # run authentication + self.coms.push(self.INTERNAL_AUTHENTICATE(P1=0x00, P2=0x00, Data=RAND)) + if self.coms()[2][0] != 0x9F: + if self.dbg: + print '[DBG] %s' % self.coms() + return None + # get authentication response + self.coms.push(self.GET_RESPONSE(Le=self.coms()[2][1])) + if self.coms()[2] != (0x90, 0x00): + if self.dbg: + print '[DBG] %s' % self.coms() + return None + SRES, Kc = self.coms()[3][0:4], self.coms()[3][4:] + return [ SRES, Kc ] + + def get_imsi(self): + ''' + self.get_imsi() -> string(IMSI) + + reads IMSI value at address [0x6F, 0x07] + returns IMSI string on success or None on error + ''' + # select DF_GSM for SIM card + self.select([0x7F, 0x20]) + if self.coms()[2] != (0x90, 0x00): + if self.dbg: + print '[DBG] %s' % self.coms() + return None + + # select IMSI file + imsi = self.select([0x6F, 0x07]) + if self.coms()[2] != (0x90, 0x00): + if self.dbg: + print '[DBG] %s' % self.coms() + 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 + + diff --git a/card/USIM.py b/card/USIM.py new file mode 100644 index 0000000..d8cbb85 --- /dev/null +++ b/card/USIM.py @@ -0,0 +1,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 + + diff --git a/card/__init__.py b/card/__init__.py new file mode 100644 index 0000000..c5bafa0 --- /dev/null +++ b/card/__init__.py @@ -0,0 +1,27 @@ +""" +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. +""" + +# smartcard Integrated Circuit Card library +# based on Laurent Rousseau pcsclite daemon and Jean-Daniel Aussel pyscard binding +# specificities of SIM and USIM card available + + +__all__ = ['utils', 'ICC', 'SIM', 'USIM', 'FS'] +__version__ = '0.1.0' + diff --git a/card/utils.py b/card/utils.py new file mode 100644 index 0000000..05e7a9e --- /dev/null +++ b/card/utils.py @@ -0,0 +1,252 @@ +""" +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. +""" + +################################# +# generic functions # +# being used in smartcard specs # +################################# + +from collections import deque +from smartcard.util import toBytes + +# from python 2.6, format('b') allows to use 0b10010110 notation: +# much convinient +def byteToBit(byte): + ''' + byteToBit(0xAB) -> [1, 0, 1, 0, 1, 0, 1, 1] + + converts a byte integer value into a list of bits + ''' + bit = [0, 0, 0, 0, 0, 0, 0, 0] + for i in range(8): + if byte % pow(2, i+1): + bit[7-i] = 1 + byte = byte - pow(2, i) + return bit + +# equivalent to the pyscard function "toASCIIBytes" +# new version of python (>2.6) seems to have a built-in "bytes" type +def stringToByte(string): + ''' + stringToByte('test') -> [116, 101, 115, 116] + + converts a string into a list of bytes + ''' + bytelist = [] + for c in string: + bytelist.extend( toBytes(c.encode('hex')) ) + return bytelist + +# equivalent to the pyscard function "toASCIIString" +def byteToString(bytelist): + ''' + byteToString([116, 101, 115, 116]) -> 'test' + + converts a list of bytes into a string + ''' + string = '' + for b in bytelist: + string += chr(b) + return string + +def LV_parser(bytelist): + ''' + LV_parser([0x02, 0xAB, 0xCD, 0x01, 0x12, 0x34]) -> [[171, 205], [18], []] + + parses Length-Value records in a list of bytes + returns a list of list of bytes + length coded on 1 byte + ''' + values = [] + while len(bytelist) > 0: + l = bytelist[0] + values.append( bytelist[1:1+l] ) + bytelist = bytelist[1+l:] + return values + +def first_TLV_parser(bytelist): + ''' + first_TLV_parser([0xAA, 0x02, 0xAB, 0xCD, 0xFF, 0x00]) -> (170, 2, [171, 205]) + + parses first TLV format record in a list of bytelist + returns a 3-Tuple: Tag, Length, Value + Value is a list of bytes + parsing of length is ETSI'style 101.220 + ''' + Tag = bytelist[0] + if bytelist[1] == 0xFF: + Len = bytelist[2]*256 + bytelist[3] + Val = bytelist[4:4+Len] + else: + Len = bytelist[1] + Val = bytelist[2:2+Len] + return (Tag, Len, Val) + +def TLV_parser(bytelist): + ''' + TLV_parser([0xAA, ..., 0xFF]) -> [(T, L, [V]), (T, L, [V]), ...] + + loops on the input list of bytes with the "first_TLV_parser()" function + returns a list of 3-Tuples + ''' + ret = [] + while len(bytelist) > 0: + T, L, V = first_TLV_parser(bytelist) + if T == 0xFF: + # padding bytes + break + ret.append( (T, L, V) ) + # need to manage length of L + if L > 0xFE: + bytelist = bytelist[ L+4 : ] + else: + bytelist = bytelist[ L+2 : ] + return ret + +def first_BERTLV_parser(bytelist): + ''' + first_BERTLV_parser([0xAA, 0x02, 0xAB, 0xCD, 0xFF, 0x00]) + -> ([1, 'contextual', 'constructed', 10], [1, 2], [171, 205]) + + parses first BER-TLV format record in a list of bytes + returns a 3-Tuple: Tag, Length, Value + Tag: [Tag class, Tag DO, Tag number] + Length: [Length of length, Length value] + Value: [Value bytes list] + parsing of length is ETSI'style 101.220 + ''' + # Tag class and DO + byte0 = byteToBit(bytelist[0]) + if byte0[0:2] == [0, 0]: + Tag_class = 'universal' + elif byte0[0:2] == [0, 1]: + Tag_class = 'applicative' + elif byte0[0:2] == [1, 0]: + Tag_class = 'contextual' + elif byte0[0:2] == [1, 1]: + Tag_class = 'private' + if byte0[2:3] == [0]: + Tag_DO = 'primitive' + elif byte0[2:3] == [1]: + Tag_DO = 'constructed' + # Tag coded with more than 1 byte + i = 0 + if byte0[3:8] == [1, 1, 1, 1, 1]: + Tag_bits = byteToBit(bytelist[1])[1:8] + i += 1 + while byteToBit(bytelist[i])[0] == 1: + i += 1 + Tag_bits += byteToBit(bytelist[i])[1:8] + # Tag coded with 1 byte + else: + Tag_bits = byte0[3:8] + + # Tag number calculation + Tag_num = 0 + for j in range(len(Tag_bits)): + Tag_num += Tag_bits[len(Tag_bits)-j-1] * pow(2, j) + + # Length coded with more than 1 byte + if bytelist[i+1] > 0x50: + Len_num = bytelist[i+1] - 0x50 + Len_bytes = bytelist[i+2:i+1+Len_num] + Len = 0 + for j in range(len(Len_bytes)): + Len += bytelist[i+1+Len_num-j] * pow(256, j) + Val = bytelist[i+1+Len_num:i+1+Len_num+Len] + # Length coded with 1 byte + else: + Len_num = 1 + Len = bytelist[i+1] + Val = bytelist[i+2:i+2+Len] + + return ([i+1, Tag_class, Tag_DO, Tag_num], [Len_num, Len], Val) + #return ([Tag_class, Tag_DO, Tag_num], Len, Val) + +def BERTLV_parser(bytelist): + ''' + BERTLV_parser([0xAA, ..., 0xFF]) -> [([T], L, [V]), ([T], L, [V]), ...] + + loops on the input bytes with the "first_BERTLV_parser()" function + returns a list of 3-Tuples containing BERTLV records + ''' + ret = [] + while len(bytelist) > 0: + T, L, V = first_BERTLV_parser(bytelist) + #if T == 0xFF: + # break # padding bytes + ret.append( (T[1:], L[1], V) ) + # need to manage lengths of Tag and Length + bytelist = bytelist[ T[0] + L[0] + L[1] : ] + return ret + +def decode_BCD(data=[]): + ''' + decode_BCD([0x21, 0xFE, 0xA3]) -> '121415310' + + to decode serial number (IMSI, ICCID...) from list of bytes + ''' + string = '' + for B in data: + string += str( B & 0x0F ) + string += str( B >> 4 ) + return string + + +####################################################### +# Generic class to keep track of sent / received APDU # +####################################################### +class apdu_stack: + ''' + input / output wrapping class + for APDU communications + + allows to keep track of communications + and exchanged commands + + based on the python "deque" fifo-like object + ''' + + def __init__(self, limit=10): + ''' + initializes apdu_stack with the maximum of IO to keep track of + ''' + self.apdu_stack = deque([], limit) + + def push(self, apdu_response): + ''' + stacks the returned response into the apdu_stack + ''' + self.apdu_stack.append( apdu_response ) + + def __repr__(self): + ''' + represents the whole stack of responses pushed on + ''' + s = '' + for apdu in self.apdu_stack: + s += apdu.__repr__() + '\n' + return s + + def __call__(self): + ''' + calling the apdu_stack returns the last response pushed on it + ''' + return self.apdu_stack[-1] + diff --git a/osmo-sim-auth.py b/osmo-sim-auth.py new file mode 100755 index 0000000..a1364ab --- /dev/null +++ b/osmo-sim-auth.py @@ -0,0 +1,106 @@ +#!/usr/bin/python + +""" +Test script for (U)SIM authentication +Copyright (C) 2011 Harald Welte <laforge@gnumonks.org> + +based heavily on the "card" library by Benoit Michau and pyscard + +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. +""" + +from binascii import * +from card.utils import * +from optparse import OptionParser +from card.USIM import USIM +from card.SIM import SIM + +def handle_usim(options, rand_bin, autn_bin): + u = USIM() + if not u: + print "Error opening USIM" + exit(1) + + if options.debug: + u.dbg = 2; + + imsi = u.get_imsi() + print "Testing USIM card with IMSI %s" % imsi + + print "\nUMTS Authentication" + ret = u.authenticate(rand_bin, autn_bin, ctx='3G') + if len(ret) == 1: + print "AUTS:\t%s" % b2a_hex(byteToString(ret[0])) + else: + print "RES:\t%s" % b2a_hex(byteToString(ret[0])) + print "CK:\t%s" % b2a_hex(byteToString(ret[1])) + print "IK:\t%s" % b2a_hex(byteToString(ret[2])) + if len(ret) == 4: + print "Kc:\t%s" % b2a_hex(byteToString(ret[3])) + + print "\nGSM Authentication" + ret = u.authenticate(rand_bin, autn_bin, ctx='2G') + if not len(ret) == 2: + print "Error during 2G authentication" + exit(1) + print "SRES:\t%s" % b2a_hex(byteToString(ret[0])) + print "Kc:\t%s" % b2a_hex(byteToString(ret[1])) + +def handle_sim(options, rand_bin): + s= SIM() + if not s: + print "Error opening SIM" + exit(1) + + imsi = s.get_imsi() + print "Testing SIM card with IMSI %s" % imsi + + print "\nGSM Authentication" + ret = s.run_gsm_alg(rand_bin) + print "SRES:\t%s" % b2a_hex(byteToString(ret[0])) + print "Kc:\t%s" % b2a_hex(byteToString(ret[1])) + + +if __name__ == "__main__": + parser = OptionParser() + parser.add_option("-a", "--autn", dest="autn", + help="AUTN parameter from AuC") + parser.add_option("-r", "--rand", dest="rand", + help="RAND parameter from AuC") + parser.add_option("-d", "--debug", dest="debug", + help="Enable debug output", + action="store_true") + parser.add_option("-s", "--sim", dest="sim", + help="SIM mode (default: USIM)", + action="store_true", default=False) + + (options, args) = parser.parse_args() + + if not options.rand: + print "You have to specify RAND" + exit(2) + + rand_bin = stringToByte(a2b_hex(options.rand)) + if options.autn: + autn_bin = stringToByte(a2b_hex(options.autn)) + + if options.sim == True: + handle_sim(options, rand_bin) + else: + if not options.autn: + print "You have to specify AUTN" + exit(2) + handle_usim(options, rand_bin, autn_bin) + |