All vulnerabilities
GHSA-8m97-wwx3-r8vj

FreeRDP: Out-of-Bounds Memory Read via Improperly Terminated LocateCardsByATRA Loop

FreeRDP
CVSS
6.5Medium
ExploitationNo known exploitationNot in CISA KEV, which does not prove it is unexploitedCISA KEV
Exploit odds (EPSS)No scoreNo FIRST EPSS score in this snapshot yet
Fixed in3.28.0Fixed versionGitHub
Published29 Sep 2026Updated 29 Sep 2026GitHub

What fixes it

A fixed version on one release line is not the upgrade target for every line. Check your exact version and any vendor backports.

Description

Discovered and reported by Alex Dowad of Blackwing Intelligence.

Summary

All 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.

FreeRDP 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.

Details

In 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.

The vulnerable loop, which would perform the OOB read, is as follows:

for (UINT32 i = 0; i < call->cAtrs; i++)
{
    for (UINT32 j = 0; j < call->cReaders; j++)
    {
        for (UINT32 k = 0; k < call->rgAtrMasks[i].cbAtr; k++)
        {
            if ((call->rgAtrMasks[i].rgbAtr[k] & call->rgAtrMasks[i].rgbMask[k]) !=
                (states[j].rgbAtr[k] & call->rgAtrMasks[i].rgbMask[k]))
            {
                break;
            }

            states[j].dwEventState |= SCARD_STATE_ATRMATCH;
        }
    }
}

Since 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.

Although 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.

This 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.)

To 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.

This 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.

Since 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.

Remediation

For 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.

The stronger validation code reads as follows (libfreerdp/utils/smartcard_pack.c):

LocateCards_ATRMask* masks = *data;
for (size_t x = 0; x < min; x++)
{
        const UINT32 val = winpr_Data_Get_UINT32(&masks[x].cbAtr);
        if (val > ARRAYSIZE(masks[x].rgbAtr))
        {
                WLog_Print(log, WLOG_ERROR,
                           "LocateCards_ATRMask[%" PRIuz "]::cbAtr %" PRIu32 " exceeds %" PRIuz, x, val,
                           (size_t)ARRAYSIZE(masks[x].rgbAtr));
                free(*data);
                *data = nullptr;
                return STATUS_DATA_ERROR;
        }
}

Note 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:

for (UINT32 k = 0; k < call->rgAtrMasks[i].cbAtr; k++)

Note 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.

Impact

Combined 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.

Text from GitHub Advisory Database

Affected versions 1

  • FreeRDP>= 1.2.0-beta1, < 3.28.0GitHub