Date: June 15, 2026 Focus: Parsing, decoding, and buffer handling implementation bugs Methodology: Decompiled code verified against raw ARM assembly
This document catalogs implementation-level bugs in the SAKE cryptographic library — specifically parsing errors, buffer mishandling, integer issues, and validation gaps in the BTLE message processing path. Each finding has been verified against the raw ARM assembly to eliminate decompiler false positives.
Location: SakeCrypto_ComputeMACAndAppend (0x000150c8)
Status: VERIFIED — latent bug, not exploitable with current callers
Assembly:
000150d0: sub sp, #0x34 ; Allocate 52 bytes
000150e0: movs r1, #0x1d ; Clear 29 bytes
000150ee: blx __aeabi_memclr8 ; memclr8(sp+0x10, 29)
00015100: mov r2, r4 ; r2 = dataLen
00015102: blx __aeabi_memcpy ; memcpy(sp+0x10, data, dataLen)The buffer at sp+0x10 is 32 bytes but only 29 are cleared. If dataLen is 30-32, the last 1-3 bytes are uninitialized. However, callers limit dataLen to 29 (uVar2 < 0x1e in encrypt, uVar2 - 3 max 29 in decrypt).
Impact: Latent bug. Not exploitable with current callers, but would become a vulnerability if callers change.
Location: SakeCrypto_PerformEncryptDecrypt (0x00014f44)
Status: VERIFIED — real bug
Assembly:
00014f92: cmp.w r3, #0x100 ; Compare high counter with 256
00014f96: bcc 0x00014fba ; If < 256, continue
00014f98: mov.w r1, #0x100 ; r1 = 0x100 (sentinel)
00014f9c: movs r0, #0x0 ; r0 = 0
00014f9e: strd r0, r1, [r10] ; Store low=0, high=0x100When the high 32-bit counter reaches 0x100, it's set to 0x100 permanently. The check at the top is linkState[5] < 0x100, so once set to 0x100, the function permanently returns 0 (failure).
Impact: Device becomes permanently unable to communicate after ~4 billion messages.
Location: SakeUtil_ReadRandomBytes (0x00017518)
Status: VERIFIED — latent bug
Assembly:
; Loop reading one byte at a time from /dev/urandom
; fgetc returns -1 (EOF) on error, cast to unsigned byte = 0xFFIf fgetc returns EOF (-1), the cast to undefined1 (unsigned byte) produces 0xFF. Under normal operation /dev/urandom never returns EOF, but under resource exhaustion (fd limits, etc.), all "random" bytes become 0xFF.
Impact: Predictable random bytes under resource exhaustion.
Location: SakeKeyDB_AddEntry (0x000153ce)
Status: VERIFIED — real bug
Assembly:
; Copies 5 × 16 = 80 bytes from 'size' parameter
; No check that source buffer is >= 80 bytes
000153fc: blx __aeabi_memcpy ; Copy 16 bytes from size+0x00
00015414: blx __aeabi_memcpy ; Copy 16 bytes from size+0x10
0001542c: blx __aeabi_memcpy ; Copy 16 bytes from size+0x20
00015444: blx __aeabi_memcpy ; Copy 16 bytes from size+0x30
0001545c: blx __aeabi_memcpy ; Copy 16 bytes from size+0x40The function copies 80 bytes from the size parameter without checking that the source buffer is at least 80 bytes.
Impact: Heap/stack over-read if caller passes a smaller buffer.
Location: SakeKeyDB_FindEntryByType (0x000151fa)
Status: VERIFIED — real bug
Assembly:
000151fa: mvn r3, #0x0 ; uVar3 = 0xFFFFFFFF
000151fe: ldr r0, [r0, #0x0] ; Load dbPtr
00015202: adds r0, #0x6 ; Start of entries
00015204: adds r3, #0x1 ; Increment counter
00015206: ldrb r1, [r0, #-0x1] ; Load entry count
0001520a: cmp r1, r3 ; Check against count
0001520e: bls 0x00015224 ; If done, return NULL
00015210: adds r2, r0, #0x51 ; Advance 81 bytes
00015214: ldrb r1, [r0] ; Load device type
00015218: cmp r1, r4 ; Compare with target
0001521c: bne 0x00015204 ; Loop if not matchIf the entry count byte is corrupted (e.g., 255), the loop iterates 255 times, reading 255 × 81 = 20,655 bytes past the entry start.
Impact: Massive heap/stack overread, crash, information disclosure.
Location: SakeKeyDB_LookupEntryByType (0x00015360)
Status: VERIFIED — real bug
Assembly:
; After finding entry, adds 1 to skip device type byte
0001537a: adds r0, #0x1 ; Skip device type byteReturns pointer to entry + 1 byte (skipping device type). If entry is the last in the buffer, +1 could point past allocated memory.
Impact: Out-of-bounds read when accessing last entry.
Location: SakeClient_DecryptAndValidateServerPermit (0x0001553c)
Status: VERIFIED — real design weakness
Assembly:
00015584: movs r2, #0xc ; MAC over 12 bytes only
00015590: bl 0x00015fd0 ; Compute MAC
00015596: ldr r0, [sp,#0x1c] ; Load embedded MAC (4 bytes from decrypted data)
00015598: ldr r1, [sp,#0x0] ; Load computed MAC (first 4 bytes)
0001559a: cmp r1, r0 ; Compare only 32 bits!The MAC is only 32 bits (4 bytes), compared against bytes 12-15 of the decrypted permit. The full 128-bit CMAC is truncated.
Impact: 2^32 brute-force to forge a permit MAC.
Location: SakePermit_EncryptAndSign (0x000154a4)
Status: VERIFIED — real bug
Assembly:
; Only 1 byte of version and 1 byte of deviceType are MAC'd
; Upper 3 bytes of each 4-byte field are excluded
000154bc: ldrb r0, [r0] ; Load 1 byte of version
000154c4: ldrb r1, [r1, #0x4] ; Load 1 byte of deviceType
000154d0: blx __aeabi_memcpy ; Copy 10 proprietary bytes
000154e8: movs r2, #0xc ; MAC over 12 bytes totalThe permit struct is 18 bytes (version(4) + deviceType(4) + proprietary(10)), but only 12 bytes are MAC'd (version(1) + deviceType(1) + proprietary(10)).
Impact: Upper 3 bytes of version and deviceType are not authenticated.
Location: SakeClient_DecryptAndValidateServerPermit (0x0001553c)
Status: VERIFIED — real finding
Assembly:
0001559e: ldrb.w r0, [sp,#0x10] ; Load decrypted version byte
000155a2: str.w r0, [r9,#0x0] ; Store to output
000155a6: cbnz r0, 0x000155c8 ; If version != 0, skip device type!Only version 0 passes the check. The SAKE_PERMIT_VERSION_E enum defines VERSION_1=1, VERSION_2=2, VERSION_3=3. This means either:
- Version 0 is the only valid version (enum is wrong)
- The enum values are correct and no valid permit passes (bug)
Impact: Only version 0 permits are accepted.
Location: SakeServer_ProcessState3_DeriveAndVerify (0x000157a4)
Status: VERIFIED — real redundancy
Assembly:
; First call
000157c0: bl SakeClient_PerformKeyConfirmation
; ... verification ...
; Second call
000157e0: bl SakeClient_PerformKeyConfirmationSakeClient_PerformKeyConfirmation() is called twice in the same function. Each call generates random data and computes MACs.
Impact: Unnecessary entropy waste and timing side-channel.
These bugs exist in the JNI layer that handles unencrypted SAKE_USER_MESSAGE_S and SAKE_SECURE_MESSAGE_S before/after encryption.
Location: SakeJNI_SetUserMessageBytes (0x000140a6)
Status: VERIFIED — real bug
Assembly:
000140a6: ldr r0, [sp, #0x8] ; r0 = source (JNI byte array)
000140a8: movs r1, #0x0 ; i = 0
000140aa: cmp r1, #0x1d ; Compare with 29
000140ac: it eq
000140ae: bx.eq lr ; Return when i == 29
000140b0: ldrb r3, [r0, r1] ; r3 = source[i]
000140b2: strb r3, [r2, r1] ; dest[i] = r3
000140b4: adds r1, #0x1 ; i++
000140b6: b 0x000140aa ; LoopThe function always copies exactly 29 bytes from the JNI byte array to the SAKE_USER_MESSAGE_S.pBytes buffer. No check on the actual JNI array length. If the Java array is smaller than 29 bytes, this reads past the end of the JNI array (JNI arrays are not bounds-checked by default).
Impact: Heap over-read from JNI array. If the Java array is 10 bytes, 19 bytes of adjacent heap data are copied into the message buffer.
Location: SakeJNI_SetUserMessageByteCount (0x000140be)
Status: VERIFIED — real bug
Assembly:
000140be: cbz r2, 0x000140c4 ; If dest == NULL, return
000140c0: ldr r0, [sp, #0x8] ; r0 = byteCount value
000140c2: str r0, [r2, #0x20] ; dest->byteCount = r0
000140c4: bx lrThe byteCount is stored directly without any validation. The SAKE_USER_MESSAGE_S.pBytes buffer is only 29 bytes, but byteCount can be set to any value (e.g., 255). Downstream code that uses byteCount to index into pBytes will read/write past the buffer.
Impact: If byteCount > 29, subsequent operations using byteCount as an index will access memory past the 29-byte pBytes buffer.
Location: SakeJNI_SecureMessage_SetBytes (0x00014028)
Status: VERIFIED — real bug
Assembly:
00014028: ldr r0, [sp, #0x8] ; r0 = source (JNI byte array)
0001402a: movs r1, #0x0 ; i = 0
0001402c: cmp r1, #0x20 ; Compare with 32
0001402e: it eq
00014030: bx.eq lr ; Return when i == 32
00014032: ldrb r3, [r0, r1] ; r3 = source[i]
00014034: strb r3, [r2, r1] ; dest[i] = r3
00014036: adds r1, #0x1 ; i++
00014038: b 0x0001402c ; LoopSame bug as 3.1 but for SAKE_SECURE_MESSAGE_S. Always copies 32 bytes regardless of JNI array length.
Impact: Heap over-read from JNI array if source is smaller than 32 bytes.
Location: SakeJNI_SecureMessage_SetByteCount (0x00014040)
Status: VERIFIED — real bug
Assembly:
00014040: cbz r2, 0x00014046 ; If dest == NULL, return
00014042: ldr r0, [sp, #0x8] ; r0 = byteCount value
00014044: str r0, [r2, #0x20] ; dest->byteCount = r0
00014046: bx lrSame bug as 3.2 but for SAKE_SECURE_MESSAGE_S. byteCount can be set to any value. The encrypt path checks byteCount < 0x1E (30), so values > 29 are rejected at encryption time. But the decrypt path uses byteCount - 3 as the payload size, and if byteCount is 0, 1, or 2, the subtraction underflows.
Impact: If byteCount is set to 0-2, the decrypt path computes byteCount - 3 which wraps to a large unsigned value. The bounds check 0x1D < uVar1 catches this, but it's a fragile defense.
Location: SakeJNI_SecureMessage_GetBytes (0x0001403a)
Status: VERIFIED — design issue
Assembly:
0001403a: mov r0, r2 ; Return the struct pointer
0001403c: movs r1, #0x0
0001403e: bx lrThe getter returns the raw pointer to the SAKE_SECURE_MESSAGE_S struct, not a copy of the bytes. The Java side receives a native pointer that it can read from directly. If the struct is freed or modified concurrently, the Java side will read stale or corrupted data.
Impact: Potential use-after-free or data race if the struct is modified while Java holds the pointer.
Location: SakeClient_DecryptAndValidateServerPermit (0x0001553c)
The decompiler showed local_48[0] == local_2c where local_2c = 0. But assembly verification reveals:
sp+0x1Cis bytes 12-15 of the decrypted permit data (overwritten bySakeCrypto_AES_ECB_SingleBlockEncrypt)- The comparison is
computed_MAC[0..3] == decrypted_permit[12..15] - The MAC is compared against the embedded MAC in the permit, not zero
The decompiler was confused because local_2c was initialized to 0, but the decrypted data overwrites it.
Location: SakeServer_ProcessState4_DecryptPermit (0x000158d4)
The SAKE_DEVICE_TYPE_E enum is 4 bytes, so the 4-byte comparison is correct.
Location: sakeCrypto_DecryptAuthenticate_HighLevel (0x0001512c)
Assembly shows the return value IS checked: mov r4, r0 stores the result, and b 0x0001518e jumps to the exit where r4 is returned.
Location: SakeKeyDB_OpenDatabase (0x00015290)
Assembly shows strd r5, r2, [sp,#0x0] stores dataBuffer at sp+0, then mov r0, sp passes sp to SakeKeyDB_ValidateCRC. The function dereferences *param_1 to get dataBuffer, which is correct.
Location: SakeClient_VerifyHandshakeMAC (0x00015e3c)
The decompiler showed local_30[2] as 8 bytes, but the actual stack layout has more space. The SakeCrypto_Memcmp call compares the computed MAC against the expected MAC stored at a valid stack location.
| # | Bug | Location | Severity | Status |
|---|---|---|---|---|
| 2.1 | Stack under-init in MAC | 0x000150c8 |
LOW | VERIFIED (latent) |
| 2.2 | Counter permanent lockout | 0x00014f44 |
MEDIUM | VERIFIED |
| 2.3 | EOF → 0xFF in random | 0x00017518 |
LOW | VERIFIED (latent) |
| 2.4 | Key DB source overread | 0x000153ce |
MEDIUM | VERIFIED |
| 2.5 | Key DB entry overread | 0x000151fa |
MEDIUM | VERIFIED |
| 2.6 | Key DB entry pointer +1 | 0x00015360 |
LOW | VERIFIED |
| 2.7 | Permit MAC truncation (32-bit) | 0x0001553c |
MEDIUM | VERIFIED |
| 2.8 | Permit field truncation in MAC | 0x000154a4 |
LOW | VERIFIED |
| 2.9 | Permit version gate=0 | 0x0001553c |
LOW | VERIFIED |
| 2.10 | Double key confirmation | 0x000157a4 |
LOW | VERIFIED |
| 3.1 | UserMessage fixed 29-byte copy | 0x000140a6 |
MEDIUM | VERIFIED |
| 3.2 | UserMessage byteCount no bounds | 0x000140be |
MEDIUM | VERIFIED |
| 3.3 | SecureMessage fixed 32-byte copy | 0x00014028 |
MEDIUM | VERIFIED |
| 3.4 | SecureMessage byteCount no bounds | 0x00014040 |
MEDIUM | VERIFIED |
| 3.5 | SecureMessage getter returns raw ptr | 0x0001403a |
LOW | VERIFIED |
| Priority | Bug | Fix |
|---|---|---|
| P1 | 2.2 Counter lockout | Wrap high counter to 0 instead of 0x100 |
| P1 | 2.7 MAC truncation | Use full 128-bit CMAC or at least 64-bit |
| P1 | 3.1 UserMessage fixed copy | Copy only min(arrayLength, 29) bytes |
| P1 | 3.2 UserMessage byteCount | Validate byteCount <= 29 before storing |
| P1 | 3.3 SecureMessage fixed copy | Copy only min(arrayLength, 32) bytes |
| P1 | 3.4 SecureMessage byteCount | Validate byteCount <= 32 before storing |
| P2 | 2.4 Key DB overread | Add source buffer size validation |
| P2 | 2.5 Entry count bounds | Validate entry count against buffer size |
| P2 | 2.8 Permit field truncation | MAC full 18-byte permit |
| P3 | 2.1 Stack under-init | memclr8(buf, 0x20) instead of 0x1d |
| P3 | 2.3 EOF handling | Check fgetc return for EOF |
| P3 | 2.6 Entry pointer +1 | Validate pointer bounds |
| P3 | 2.9 Version gate | Accept valid enum versions |
| P3 | 2.10 Double confirmation | Remove redundant call |
| P3 | 3.5 Raw pointer return | Return copy or use reference counting |
Each finding was verified through:
- Decompilation — Ghidra decompiler output analyzed
- Assembly verification — Raw ARM assembly inspected for each claim
- False positive elimination — Decompiler artifacts identified and removed
- Caller analysis — Verified whether callers constrain inputs to prevent exploitation
The library works correctly in production because:
- Callers limit input sizes to safe ranges
- The 32-bit MAC comparison against embedded data is functionally correct
- Stack under-initialization doesn't affect actual MAC values due to caller constraints