{"schema":"threatline.record/2","record":{"id":"GHSA-8m97-wwx3-r8vj","aliases":["GHSA-8m97-wwx3-r8vj"],"summary":"Out-of-Bounds Memory Read via Improperly Terminated LocateCardsByATRA Loop","details":"Discovered and reported by Alex Dowad of Blackwing Intelligence.\r\n\r\n### Summary\r\n\r\nAll releases of FreeRDP from 1.2.0-beta1 up to 3.27.1 contained a bug in the handler for LocateCardsByATRA messages which could be used to read out heap memory, one bit at a time. This bug could be used to defeat ASLR and leak other information needed to exploit memory corruption vulnerabilities and achieve RCE. During an authorized penetration test, our researcher combined this bug with a separate use-after-free vulnerability to achieve RCE against an installation of FreeRDP 3.21.0.\r\n\r\nFreeRDP 3.28.0 includes strengthened validation code which partially mitigates the vulnerability. However, due to a separate bug, the validation code only works as intended on little-endian CPU architectures. FreeRDP supports one big-endian architecture, s390x, and users on that hardware platform are still at risk. Users on little-endian platforms are protected if they upgrade to FreeRDP 3.28.0 or later.\r\n\r\n### Details\r\nIn smartcard_LocateCardsByATRA_Call (libfreerdp/utils/smartcard_call.c), FreeRDP searches an array of smartcard readers for a record whose ATR matches any one of multiple provided ATR patterns. Each pattern has a byte count, bit mask, and value. The byte count should be capped to 36, but FreeRDP failed to do this. Hence, the caller could provide any 32-bit byte count at all, and FreeRDP would compare the provided number of bytes with the provided pattern.\r\n\r\nThe vulnerable loop, which would perform the OOB read, is as follows:\r\n\r\n```c\r\nfor (UINT32 i = 0; i < call->cAtrs; i++)\r\n{\r\n    for (UINT32 j = 0; j < call->cReaders; j++)\r\n    {\r\n        for (UINT32 k = 0; k < call->rgAtrMasks[i].cbAtr; k++)\r\n        {\r\n            if ((call->rgAtrMasks[i].rgbAtr[k] & call->rgAtrMasks[i].rgbMask[k]) !=\r\n                (states[j].rgbAtr[k] & call->rgAtrMasks[i].rgbMask[k]))\r\n            {\r\n                break;\r\n            }\r\n\r\n            states[j].dwEventState |= SCARD_STATE_ATRMATCH;\r\n        }\r\n    }\r\n}\r\n```\r\n\r\nSince each ATR pattern is just 76 bytes, it might seem that large values of `cbAtr` will cause FreeRDP to do a pattern match between two random regions of memory, which will almost certainly fail quickly. But since the number of ATR patterns provided is arbitrary (and attacker-controlled), the attacker can provide an array with thousands of ATR patterns, and fill them with a crafted bit pattern. During the pattern match operation, FreeRDP will walk off the end of the first ATR pattern, using the remaining ATR patterns as part of a single huge “ATR pattern”, with overlapping mask and value. By setting the contents of the ATR pattern array appropriately, the attacker can probe a single bit in memory, relative to (and after) the `cReaders` array. The probed bit can potentially be tens of thousands of bytes after the end of the `cReaders` array.\r\n\r\nAlthough the result of the out-of-bounds read is not returned directly to the (malicious) RDP server in the output of LocateCardsByATRA, it affects the runtime of the query. By including a suitable number of zero bytes at the tail of the ATR pattern array, the attacker can amplify this timing signal so that it is clearly detectable. LocateCardsByATRA then becomes a timing oracle which leaks one bit of memory. Through repeated LocateCardsByATRA queries, the malicious server can recover the contents of an entire word or struct.\r\n\r\nThis only works if `cReaders` lands at a consistent location in memory for each LocateCardsByATRA query. The best way for the attacker to ensure this is by using an “unusual” size for the `cReaders` array, which does not coincide with any other memory chunk size allocated by FreeRDP during normal operation. The attacker also needs to cause FreeRDP to allocate and free 7 or more chunks of the same size first, to fill up the corresponding bucket in glibc’s tcache. This is not a challenge; there are many ways to cause FreeRDP to allocate and free a chunk of any desired size. (This applies to FreeRDP when linked with glibc 2.39, the version used by our penetration test's target. However, even if FreeRDP is linked with a different libc, similar grooming techniques might still be used to achieve a desired heap layout.)\r\n\r\nTo achieve an ASLR bypass, we used a ConnectA smartcard query to cause FreeRDP to allocate an OpenSSL X509 certificate object shortly after the memory chunk which was groomed for `cReaders`. The certificate object contains multiple pointers to known offsets within libcrypto, which are easily identifiable from their low bits. The middle bits of those pointers revealed the ASLR slide used for mapping the main program binary and library binaries.\r\n\r\nThis bug can only be used to read out the thread heap used by the thread processing a LocateCardsByATRA query, and only the portion of that heap which follows the chunk groomed for the `cReaders` array. In the FreeRDP version which was the subject of our testing (3.21.0), it does not appear that extremely sensitive information such as passwords or private keys for TLS certificates are ever stored in that region of memory. Hence, the primary utility of this vulnerability to an attacker lies in defeating ASLR, and leaking other values which may be required to exploit a separate memory-safety bug.\r\n\r\nSince reading out memory one bit at a time is slow, and a separate memory leak bug limited the number of oracle queries we could make before FreeRDP would be OOM-killed, our RCE exploit relied on heap grooming to get important values at known offsets from `cReaders`, so we could use targeted oracle queries rather than blindly scanning the entire heap. Such heap-grooming code is highly sensitive to the exact sequence of allocations and frees executed by specific code paths in FreeRDP, and can easily break when moving to a different FreeRDP version. Further, such grooming code is also sensitive to the versions of libc and other libraries. So it seems that any real-world exploits based on this bug might need to be tailored to a specific target system.\r\n\r\n### Remediation\r\nFor systems with little-endian CPUs, this bug was fixed via stricter validation code added in `ececc5cbd0646ef012ca178322e6ef922a11f0c1`, and the fix is included in all versions starting from 3.28.0.\r\n\r\nThe stronger validation code reads as follows (libfreerdp/utils/smartcard_pack.c):\r\n\r\n```c\r\nLocateCards_ATRMask* masks = *data;\r\nfor (size_t x = 0; x < min; x++)\r\n{\r\n        const UINT32 val = winpr_Data_Get_UINT32(&masks[x].cbAtr);\r\n        if (val > ARRAYSIZE(masks[x].rgbAtr))\r\n        {\r\n                WLog_Print(log, WLOG_ERROR,\r\n                           \"LocateCards_ATRMask[%\" PRIuz \"]::cbAtr %\" PRIu32 \" exceeds %\" PRIuz, x, val,\r\n                           (size_t)ARRAYSIZE(masks[x].rgbAtr));\r\n                free(*data);\r\n                *data = nullptr;\r\n                return STATUS_DATA_ERROR;\r\n        }\r\n}\r\n```\r\n\r\nNote that the validation code (correctly) reads the sensitive value using `winpr_Data_Get_UINT32`. This interprets `cbAtr` as an integer in network byte order, which is appropriate, since it is read off the wire and stored directly in memory without converting to host byte order. However, the consumer of `cbAtr` uses it in this way:\r\n\r\n```c\r\nfor (UINT32 k = 0; k < call->rgAtrMasks[i].cbAtr; k++)\r\n```\r\n\r\nNote that `cbAtr` is consumed as an integer in host byte order, meaning that the value is effectively byte-reversed on an s390x system. Since the validator and consumer interpret the value in a completely different way, the validation can be bypassed. However, `cbAtr` values which pass validation will become enormous when byte-reversed, which makes it more difficult (but not impossible) to use the bug for memory disclosure. The key to using this bug for memory disclosure, post 3.28.0 and on a s390x target, would be to include a pattern at the end of the ATR pattern array which always fails to match in practice, thus effectively terminating the pattern match loop without relying on `cbAtr`.\r\n\r\n### Impact\r\nCombined with another vulnerability, this bug can enable RCE, leading to a complete takeover of a vulnerable system. However, if FreeRDP is compiled with smartcard-related features disabled, then this bug is not exploitable.","categories":["Unclassified"],"product":"FreeRDP","vendor":"","ecosystem":null,"affected_ranges":[{"source":"GitHub Advisory Database","source_url":"https://github.com/FreeRDP/FreeRDP/security/advisories/GHSA-8m97-wwx3-r8vj","source_modified_at":"2026-09-29T09:13:37Z","fetched_at":"2026-10-23T18:48:36.087640Z","package":{"ecosystem":"","name":"FreeRDP"},"range":">= 1.2.0-beta1, < 3.28.0"}],"fixed_versions":[{"source":"GitHub Advisory Database","source_url":"https://github.com/FreeRDP/FreeRDP/security/advisories/GHSA-8m97-wwx3-r8vj","source_modified_at":"2026-09-29T09:13:37Z","fetched_at":"2026-10-23T18:48:36.087640Z","package":{"ecosystem":"","name":"FreeRDP"},"version":"3.28.0"}],"cvss":6.5,"severity":"Medium","kev":{"listed":false},"epss":{"probability":null,"percentile":null,"score_date":null},"published_at":"2026-09-29T09:13:37Z","modified_at":"2026-09-29T09:13:37Z","provenance":[{"source":"GitHub Advisory Database","url":"https://github.com/FreeRDP/FreeRDP/security/advisories/GHSA-8m97-wwx3-r8vj","fetched_at":"2026-10-23T18:48:36.087640Z","source_modified_at":"2026-09-29T09:13:37Z"}],"sources":["GitHub Advisory Database"],"references":[],"locations":[],"motoko_assessment":{"status":"requires_architecture_review","automatic_remediation":false},"classification":{"method":"product-first software-layer rules","rules_version":"2.1","matched_on":null,"confidence":"unknown","reviewed":false},"change_timestamp":"2026-09-29T09:13:37Z","content_hash":"ffc04fd9f22f212038b66260ad00cdc0b3b59320b753481c9f239d90cc7793b3"}}
