3. JCOP Applet基础:生命周期、APDU协议与执行流程
各位同学,今天我们来聊聊JCOP Applet的基础。这部分内容,说白了就是Java Card世界里最核心的骨架。你想想看,没有这些基础,后面那些漏洞利用技巧根本玩不转。我个人习惯把Applet比作一个「微型服务」,它运行在智能卡这个极度受限的环境里。
3.1 Applet生命周期:从生到死
每个JCOP Applet都有固定的生命周期。嗯,这里要注意,它不像普通Java程序那样随用随跑。Applet的生死,完全由Java Card运行时环境(也就是JCRE)控制。
生命周期分为四个阶段:
- 安装(Install):Applet被加载到卡片上,分配内存空间。我在项目中遇到过,有些开发者在安装阶段没处理好内存分配,结果后面运行时直接OOM(内存溢出)。
- 注册(Register):Applet向JCRE注册自己的AID(应用标识符)。这一步很关键,注册失败的话,终端根本找不到这个Applet。
- 选择(Select):终端通过SELECT命令选中Applet。只有被选中的Applet才能处理后续的APDU命令。
- 取消选择(Deselect):终端切换到其他Applet,或者卡片复位。当前Applet被释放。
核心要点:Applet一旦安装,就会常驻卡片内存。除非你显式删除它,否则它一直在那。这一点和Web应用很不一样。
我给你们画个流程图,这样更直观:
3.2 APDU协议基础:卡片与终端的对话语言
APDU,全称是Application Protocol Data Unit。说白了,就是终端和智能卡之间通信的「信封」。你想想看,卡片没有屏幕没有键盘,怎么跟外界交流?全靠APDU。
APDU分为两种:
- 命令APDU(C-APDU):终端发给卡片的指令。
- 响应APDU(R-APDU):卡片返回给终端的结果。
命令APDU的结构是这样的:
| 字段 | 长度(字节) | 说明 |
|---|---|---|
| CLA | 1 | 指令类别,比如0x00表示ISO标准命令 |
| INS | 1 | 指令代码,比如0xA4表示SELECT |
| P1 | 1 | 参数1 |
| P2 | 1 | 参数2 |
| Lc | 0或1或3 | 后续数据长度 |
| Data | 可变 | 实际数据内容 |
| Le | 0或1或2 | 期望返回的数据长度 |
我的经验:CLA字段经常被用来做「通道选择」。有些卡片用CLA的高位来区分安全通道和普通通道。我曾经见过一个漏洞,就是因为CLA字段没校验,导致攻击者可以绕过安全通道直接发命令。
3.3 APDU命令与响应:实战中的握手
响应APDU的结构相对简单:
| 字段 | 长度(字节) | 说明 |
|---|---|---|
| Data | 可变 | 返回的数据 |
| SW1 | 1 | 状态字节1 |
| SW2 | 1 | 状态字节2 |
常见的状态码:
0x9000:成功。这是最想看到的。0x6A82:文件或Applet未找到。0x6985:条件不满足,比如安全状态不对。0x6700:长度错误。
我给你们看个实际例子。假设我们要发送一个SELECT命令,选中某个Applet:
// 命令APDU:00 A4 04 00 07 A0 00 00 00 03 00 00 00
CLA = 0x00 // ISO标准
INS = 0xA4 // SELECT
P1 = 0x04 // 按名称选择
P2 = 0x00 // 第一个或唯一一个
Lc = 0x07 // AID长度7字节
Data= A0 00 00 00 03 00 00 // AID值
Le = 0x00 // 期望返回数据
// 响应APDU:6F 0A 84 07 A0 00 00 00 03 00 00 90 00
// 其中6F是FCI模板,84是AID标签,90 00表示成功
注意:Le字段如果设为0x00,表示期望返回最多256字节。但有些卡片实现有bug,你设0x00它反而返回0字节。我在调试一个支付卡时就踩过这个坑。
3.4 Applet注册与安装:让卡片认识你的代码
安装Applet,本质上就是往卡片里「塞」一段可执行代码。但这个过程比想象中复杂。
安装流程大致如下:
- 加载(Load):通过LOAD命令把CAP文件(Java Card的编译产物)传到卡片。
- 安装(Install):通过INSTALL命令触发Applet的安装过程。
- 注册(Register):Applet的install()方法内部调用register()方法,向JCRE注册AID。
代码层面,一个最简单的Applet长这样:
public class MyApplet extends Applet {
// 安装方法,JCRE调用
public static void install(byte[] bArray, short bOffset, byte bLength) {
MyApplet app = new MyApplet();
app.register(); // 注册AID,这一步不能少
}
// 选择方法,终端SELECT时触发
public boolean select() {
return true; // 返回true表示接受选择
}
// 取消选择方法
public void deselect() {
// 清理工作
}
// 处理APDU命令
public void process(APDU apdu) {
byte[] buffer = apdu.getBuffer();
// 解析CLA、INS等
// 处理业务逻辑
// 返回响应
}
}
关键点:register()方法必须在install()里调用。我曾经见过一个案例,开发者把register()放在了构造函数里,结果Applet安装成功了但永远无法被选中。因为JCRE只认install()里的注册行为。
3.5 Applet选择与执行:终端如何找到你的服务
终端选择Applet,靠的是AID匹配。AID最长16字节,前5字节是RID(注册标识符),后面是PIX(扩展标识符)。
选择过程是这样的:
- 终端发送SELECT命令,带上目标AID。
- JCRE遍历已注册的Applet列表,找匹配的AID。
- 找到后,调用该Applet的select()方法。
- 如果select()返回true,该Applet成为当前选中Applet。
- 后续所有APDU命令都发给这个Applet处理。
执行流程更简单:
- Applet的process()方法被调用。
- 方法内部解析APDU,执行逻辑。
- 通过apdu.setOutgoingAndSend()返回结果。
我的习惯:在process()方法里,我通常会先判断CLA和INS。如果是不认识的指令,直接抛出ISOException,返回0x6D00(指令不支持)。这样既规范,又能避免一些奇怪的错误。
嗯,到这里,JCOP Applet的基础知识就讲完了。这些内容看起来简单,但它们是后面所有漏洞利用技巧的基石。你想想看,不理解生命周期,你怎么知道什么时候注入代码最合适?不理解APDU协议,你怎么构造恶意命令?
我个人建议,初学者最好自己动手写一个简单的Applet,用GP工具(比如GlobalPlatformPro)安装到卡片上,然后用脚本发APDU命令测试。纸上得来终觉浅,绝知此事要躬行。
公众号:蓝海资料掘金营,微信deep3321