点击蓝字
关注我们
声明
本文作者:shadowabi
本文字数:8052字
阅读时长:约15分钟
附件/链接:点击查看原文下载
本文属于【狼组安全社区】原创奖励计划,未经许可禁止转载
由于传播、利用此文所提供的信息而造成的任何直接或者间接的后果及损失,均由使用者本人负责,狼组安全团队以及文章作者不为此承担任何责任。
狼组安全团队有对此文章的修改和解释权。如欲转载或传播此文章,必须保证此文章的完整性,包括版权声明等全部内容。未经狼组安全团队允许,不得任意修改或者增减此文章内容,不得以任何方式将其用于商业目的。
❝
这里是 shadowabi 的一篇跨界技术文章,学的比较基础,敬请见谅。
这次我们来学习一下银行卡 APDU 指令。
前置条件
一张银行卡
一个读卡器: ACR122U
可选项:PS/SC命令发送器(用于发送 APDU指令),EMV TLV查询分析器(解析返回的数据)。文末会有脚本直接代替这两个功能
APDU 的作用
APDU:ApplicationProtocolDataUnit(应用协议数据单元),是一种数据格式,用于在应用层协议进行通信,可用于智能卡和其他设备之间进行通信,例如刷POS机付款就是 POS 机和银行卡之间利用 APDU 指令进行通信。
当我们通过 APDU 指令和银行卡进行通信的时候,是可以获取到银行卡内的一些静态数据的。在非常早期的时候,支付宝还是支付宝钱包的时代,那时候可以通过手机 NFC 贴一下银行卡,就能获取到里面的持卡人姓名、身份证、余额、交易记录等信息,就是通过 NFC 先建立和银行卡的连接,然后再发送 APDU 指令实现的信息获取。
APDU 的结构
首先我们需要了解 APDU 的结构才能构造出相应的指令进行发送
APDU 分为命令 APUD 和响应 APDU,分别对应发送数据和接受数据。
命令 APDU 的结构是:
CLA INS P1 P2 Lc DATA Le
CLA 是指令类型,INS 是指令代码,P1 和 P2 是偏移地址,Lc 是 DATA 的长度,Le 是希望响应时回答的数据字节数,0 代表最大可能长度
其中 CLA INS P1 P2 是必须存在的,Lc 和 Le 是可选的
例如下面的指令:
CLA | INS | P1 | P2 | Lc | DATA | Le |
---|---|---|---|---|---|---|
00 | A4 | 04 | 00 | 08 | A0 00 00 03 33 01 01 01 |
CLA = 00, INS = A4,P1 = 04,P2 = 00, Lc = 08, DATA = A0 00 00 03 33 01 01 01
这里没写 Le,默认是 00 即 获取最大响应长度
根据 ISO 7816-4 的通信规范,这个指令的意思是:选择下一个文件,应用标识符是中国银联借记卡。其中选择文件由(CLA = 00, INS = A4)控制,“下一个”由(P1 = 04)控制,“中国银联借记卡”由(DATA=A0 00 00 03 33 01 01 01)控制
常见的指令动作如下:
在这里,由于我们仅需要去读取银行卡的信息,因此只需要关注三个动作:
SELECT FILE, READ RECORD, GET DATA
在上面的示例中,我们直接通过 DATA = A0 00 00 03 33 01 01 01 来精准识别目标卡是银联借记卡,这个值是怎么来的呢。
这里有两种方法,第一种是通过标准规范文件去查询,另一种则是已知这张卡就是银联借记卡,直接搜对应的应用标识符(AID)。
获取 AID
第一种方法,通过 2PAY.SYS.DDF01 查询AID,无论是 EMV (Europay、Mastercard和Visa)还是 PBOC(中国银联),他们都可以通过特定的标识符 2PAY.SYS.DDF01 来定位 AID。
2PAY.SYS.DDF01 代表了 Visa邻近支付系统环境- PPSE,在 PBOC 中,QPBOC(快速的借记贷记)交易方式也是用的 2PAY.SYS.DDF01。
和之前的操作类似,我们先确认要使用的动作是选择文件:
00 A4 04 00
然后,我们将 2PAY.SYS.DDF01 进行 hex 编码,得到:325041592e5359532e4444463031 ,按照两个数字为一组划分为字节数组:
32 50 41 59 2e 53 59 53 2e 44 44 46 30 31
这就是 DATA,然后根据上面的字节数计算长度为 14,转换为十六进制为 0E,可以得到 Lc 为 0E
所以整个命令是:
00 A4 04 00 0E 32 50 41 59 2e 53 59 53 2e 44 44 46 30 31
此时我们发送命令,会得到响应 APDU:
这里简单说下响应 APDU 的结构,非常简单,只有响应体和状态码两种
Response SW1 SW2
其中 SW1 和 SW2 共同控制响应状态,90 00 代表操作成功,其他都是操作失败
在这里,我们得到了 Response 是:
6F 30 84 0E 32 50 41 59 2E 53 59 53 2E 44 44 46 30 31 A5 1E BF 0C 1B 61 19 4F 08 A0 00 00 03 33 01 01 01 50 0A 50 42 4F 43 20 44 65 62 69 74 87 01 01
这是一组 BER-TLV 数据,BER-TLV 数据的解析方式是:
头部1、2个字节代表 Tag,后面一个字节代表 length,根据 length 解析后面的数据内容
比如这里,6F 是 Tag,30 代表数据长度(注意这是十六进制数,实际长度是48),后面48个字节都是数据:84 0E 32 50 41 59 2E 53 59 53 2E 44 44 46 30 31 A5 1E BF 0C 1B 61 19 4F 08 A0 00 00 03 33 01 01 01 50 0A 50 42 4F 43 20 44 65 62 69 74 87 01 01
关于 Tag 的处理是根据 PBOC 的 TLV 解析模版来完成的,这里我们不需要太过深究。可以直接用工具来完成。
我们将刚刚得到的数据放入 EMV TLV查询分析器 中进行查询:
可以发现,AID 为 A000000333010101,和文章最开始演示的是一样的,这是中国银联借记卡的 AID。
这是一个通用的方法,另一种方法是,直接查对应的AID:
https://www.cnblogs.com/merray/p/9558197.html
这里我给出几个可能比较常用的AID
AID | 描述 |
---|---|
A000000333010101 | PBOC Debit |
A000000333010103 | PBOC Quasi Credit |
A000000333010106 | PBOC Electronic Cash |
A000000003101001 | VISA Credit |
A0000000031010 | VISA Debit/Credit (Classic) |
读卡信息
在选择文件之后,我们接下来就可以通过 READ RECORD, GET DATA 动作来读取卡内信息了:
银联卡的偏移位置都是固定的,这里直接给出结论:
00 B2 01 0c 00 读卡主要信息,00 B2(READ RECORD), 01(P1), 0c(P2), 00(Lc)
这里,P1 控制的是读卡内第几条记录,P2控制读什么记录
在旧银行卡中,00 B2 01 0c 00 可以得到持卡人姓名身份证、磁条数据等信息
00 B2 01 1c 00 读电子交易记录
00 B2 01 5c 00 读最近十条交易记录
80 CA 9F 79 00 获取卡内余额信息,80 CA(GET DATA), 9F(P1), 79(P2), 00(Lc)
GET DATA 和 READ RECORD 有点不一样,P1 和 P2 实际上就是要读取的 Tag
80 CA 9F 78 00 读取交易限额
解析脚本
在 READ RECORD 动作中,很容易想到,可以通过爆破 P1 、P2 来获取信息
因此,我们可以通过写一个简单的脚本对此进行批量爆破。但首先要解决的一个问题是,我们需要去解析 TLV 数据,面对有多重嵌套和不定长 Tag 的情况,需要正确解析 Tag Length Value。
为此,经过查找,我发现一个大佬的解析脚本:https://blog.csdn.net/u011082160/article/details/127372461
然后,简单转换一下大佬的解析脚本为 python语言,就能实现目标了。
下面脚本需要安装 pyscard 模块
pip install pyscard
from smartcard.System import readers
from smartcard.util import toHexString, toBytes
from re import search
class TLV:
def __init__(self, tag, length, value):
self.tag = tag
self.length = length
self.value = value
def hex_string_to_byte_array(hex_str):
return bytes.fromhex(hex_str)
def byte_array_to_hex_string(byte_array):
return byte_array.hex().upper()
class MyTlvDecode:
def decode_tlv(self, tlv_hex_str, tlvs):
if not tlv_hex_str:
return
if len(tlv_hex_str) % 2 != 0:
return
bytes_data = hex_string_to_byte_array(tlv_hex_str)
self._decode_tlv(bytes_data, len(bytes_data), tlvs)
def _decode_tlv(self, tlv_bytes, length, tlvs):
if not tlv_bytes or len(tlv_bytes) == 0:
return
# print(byte_array_to_hex_string(tlv_bytes))
tag = None
v_length = None
l_length = None
complex_tag = False
if (tlv_bytes[0] & 0x20) != 0x20:
if (tlv_bytes[0] & 0x1f) != 0x1f:
v_length = tlv_bytes[2] if tlv_bytes[1] == 0x81 else tlv_bytes[1]
l_length = 3 if v_length > 0x80 else 2
tag = tlv_bytes[0:1]
else:
v_length = tlv_bytes[3] if tlv_bytes[2] == 0x81 else tlv_bytes[2]
l_length = 4 if v_length > 0x80 else 3
tag = tlv_bytes[0:2]
else:
complex_tag = True
if (tlv_bytes[0] & 0x1f) != 0x1f:
v_length = tlv_bytes[2] if tlv_bytes[1] == 0x81 else tlv_bytes[1]
l_length = 3 if v_length > 0x80 else 2
tag = tlv_bytes[0:1]
else:
v_length = tlv_bytes[3] if tlv_bytes[2] == 0x81 else tlv_bytes[2]
l_length = 4 if v_length > 0x80 else 3
tag = tlv_bytes[0:2]
if v_length < 0:
raise RuntimeError(f"TLV解码异常, vLength: {v_length}")
# 分别解析出T、L、V
tag_str = byte_array_to_hex_string(tag)
value = tlv_bytes[l_length: l_length + v_length]
tag_value = byte_array_to_hex_string(value)
tag_length = len(value)
tlvs.append(TLV(tag_str, tag_length, tag_value))
if complex_tag:
self._decode_tlv(value, tag_length, tlvs)
if length > v_length + l_length:
next_tlv = tlv_bytes[v_length + l_length:]
self._decode_tlv(next_tlv, len(next_tlv), tlvs)
def BruteResponse(command):
for j in range(1,256):
command[3] = j
PrintResult(command)
def BruteRecord(command):
for i in range(1, 256):
command[2] = i
PrintResult(command)
def SingleRecord(command):
PrintResult(command)
def PrintResult(command):
response, sw1, sw2 = connection.transmit(command)
# 检查响应状态字节,确保操作成功
if sw1 == 0x90 and sw2 == 0x00:
print("command:",bytes(command).hex())
readPesponse(response)
print("------------")
def readPesponse(response):
hex_str = bytes(response).hex()
tlvs = []
decoder = MyTlvDecode()
decoder.decode_tlv(hex_str, tlvs)
print(f"RAWValue: {hex_str}")
for tlv in tlvs:
if tlv.length == 0:
continue
bytesValue = bytes.fromhex(tlv.value)
# 去除无效数据
match = search(b'[^\x00]', bytesValue)
if match:
index = match.start()
bytesValue = bytesValue[index:]
rawValue = bytesValue.hex()
tag = tlv.tag
if tag == "70":
continue
if tag == "4F":
tag = "银行卡AID"
elif tag == "57":
tag = "卡号"
bytesValue = bytes(tlv.value.split("D")[0].encode())
elif tag == "5F20":
tag = "持卡人姓名"
elif tag == "9F1F":
tag = "磁条1自定义数据"
elif tag == "9F61":
tag = "持卡人身份证"
elif tag == "6F12" or tag == "9F12":
tag = "IC卡类型"
elif tag == "50":
tag = "银行卡类型"
elif tag == "C2":
tag = "银行类型"
elif tag == "9F79":
tag = "银行卡余额"
elif tag == "9F78":
tag = "电子交易限额"
elif tag == "9F74":
tag = "交易授权码"
elif tag == "9F62":
if rawValue == "00":
bytesValue = "身份证".encode("gbk")
tag = "持卡人证件类型"
else:
continue
value = bytesValue.decode("utf8","ignore").strip()
gbkValue = bytesValue.decode("gbk","ignore").strip()
if value == "":
value = "已加密"
if gbkValue == "":
gbkValue = "已加密"
print(f"Tag: {tag},Length: {tlv.length}nrawValue: {rawValue}nUTF8Value: {value}nGBKValue: {gbkValue}")
print("n")
def ReadInfo():
command = [0x00, 0xB2, 0x01, 0x0c, 0x00] # 读卡主信息
BruteRecord(command)
command = [0x00, 0xB2, 0x01, 0x1c, 0x00] # 读电子交易信息
BruteRecord(command)
command = [0x00, 0xB2, 0x01, 0x5c, 0x00] # 读交易记录
BruteRecord(command)
command = [0x80, 0xCA, 0x9F, 0x79, 0x00] # 读余额
SingleRecord(command)
command = [0x80, 0xCA, 0x9F, 0x78, 0x00] # 读电子现金单笔交易限额
SingleRecord(command)
if __name__ == "__main__" :
try:
# 获取读卡器列表并选择一个读卡器
reader_list = readers()
reader = reader_list[0] # 选择第一个读卡器
# 连接到读卡器并选择适当的卡片
connection = reader.createConnection()
connection.connect()
except:
print("未读到卡片")
exit(0)
# EMV 通用
# command = [0x00, 0xA4, 0x04, 0x00, 0x0E, 0x32, 0x50, 0x41, 0x59, 0x2E, 0x53, 0x59, 0x53, 0x2E, 0x44, 0x44, 0x46, 0x30, 0x31] # VISA PPSE 2PAY.SYS.DDF01 315041592E5359532E4444463031
# SingleRecord(command)
# PBOC Debit
# command = [0x00, 0xA4, 0x04, 0x00, 0x08, 0xA0, 0x00, 0x00, 0x03, 0x33, 0x01, 0x01, 0x01] # PBOC A000000333010101
# SingleRecord(command)
# ReadInfo()
# PBOC Quasi Credit
# command = [0x00, 0xA4, 0x04, 0x00, 0x08, 0xA0, 0x00, 0x00, 0x03, 0x33, 0x01, 0x01, 0x03] # PBOC A000000333010103
# SingleRecord(command)
# ReadInfo()
# PBOC Electronic Cash
# command = [0x00, 0xA4, 0x04, 0x00, 0x08, 0xA0, 0x00, 0x00, 0x03, 0x33, 0x01, 0x01, 0x06] # PBOC A000000333010106
# SingleRecord(command)
# ReadInfo()
# VISA Credit
# command = [0x00, 0xA4, 0x04, 0x00, 0x08, 0xA0, 0x00, 0x00, 0x00, 0x03, 0x10, 0x10, 0x01] # VISA A000000003101001
# SingleRecord(command)
# ReadInfo()
# VISA Debit/Credit (Classic)
# command = [0x00, 0xA4, 0x04, 0x00, 0x07, 0xA0, 0x00, 0x00, 0x00, 0x03, 0x10, 0x10] # VISA A0000000031010
# SingleRecord(command)
# ReadInfo()
connection.disconnect()
在脚本中,我定义了两个方法,BruteRecord 用于爆破 P1,BruteResponse 用于 爆破 P2,BruteResponse 更多是用于研究有没有漏掉的偏移地址,有数据但是没发现的。
如果想显示所有数据的话,可以将 readPesponse 中的
else:
continue
注释掉
对应不同的AID我预留了不同的命令, 把注释解开就可以用了。
实际效果如下:
这张卡能获取到身份证,其实已经很旧了,现在的卡都是 PBOC3.0 协议,基本上对于关键信息都做了 SM4 加密,所以这里只是学习如何使用 APDU 指令去和银行卡进行交互。
作者
shadowabi
自强不息
原文始发于微信公众号(WgpSec狼组安全团队):银行卡 APDU 指令学习
- 左青龙
- 微信扫一扫
- 右白虎
- 微信扫一扫
评论