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,本质上就是往卡片里「塞」一段可执行代码。但这个过程比想象中复杂。

安装流程大致如下:

  1. 加载(Load):通过LOAD命令把CAP文件(Java Card的编译产物)传到卡片。
  2. 安装(Install):通过INSTALL命令触发Applet的安装过程。
  3. 注册(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处理。

执行流程更简单:

  1. Applet的process()方法被调用。
  2. 方法内部解析APDU,执行逻辑。
  3. 通过apdu.setOutgoingAndSend()返回结果。

我的习惯:在process()方法里,我通常会先判断CLA和INS。如果是不认识的指令,直接抛出ISOException,返回0x6D00(指令不支持)。这样既规范,又能避免一些奇怪的错误。

嗯,到这里,JCOP Applet的基础知识就讲完了。这些内容看起来简单,但它们是后面所有漏洞利用技巧的基石。你想想看,不理解生命周期,你怎么知道什么时候注入代码最合适?不理解APDU协议,你怎么构造恶意命令?

我个人建议,初学者最好自己动手写一个简单的Applet,用GP工具(比如GlobalPlatformPro)安装到卡片上,然后用脚本发APDU命令测试。纸上得来终觉浅,绝知此事要躬行。


公众号:蓝海资料掘金营,微信deep3321