CVE-2025-13032: Entering and Breaking the Avast Antivirus Sandbox Part 1
SAFA discovered four distinct kernel heap overflow vulnerabilities in Avast Antivirus. Our research targeted the aswSnx kernel driver, first requiring interesting sandbox manipulation to reach the attack surface. CVE-2025-13032 was assigned to these patched vulnerabilities. This first blog post introduces the vulnerabilities and the challenges of the custom sandbox profile. While a consecutive post will detail how the primitive was exploited for Local Privilege Escalation to System.

Introduction
At SAFA we aim to create space for our researchers to engage in technical projects that can be published and shared. We believe that sharing offensive research techniques and results serves the wider professional community, and can provide much deserved exposure to our researches and strengthens the security posture of the audited products. Since these assignments are limited in scope and time there is a special focus on selecting targets where impactful results can be achieved with moderate effort.
For this specific public research project we decided to focus on the Avast antivirus software as it is a widely deployed solution, easily accessible (freemium program, even premium features are available through a trial) and provides a rich and complex kernel attack surface through custom drivers.
Furthermore, we had no previous experience working on antivirus software so we were also driven by professional curiosity to explore them in more detail. The result of this brief engagement was the discovery of 4 exploitable kernel heap overflow and 2 local system DOS vulnerabilities. CVE-2025-13032 was assigned to these vulnerabilities.
In the rest of this blog we will detail our approach to conduct vulnerability research on the Avast sandbox kernel driver. We will briefly introduce the sandbox and the attack surface and provide a deeper technical description of the found vulnerabilities. In the following post we will explain how we exploited one of the introduced bugs to achieve a local privilege escalation to System user on an up to date Windows 11 system.
Attack Surface Analysis
Antivirus software in general has a wide attack surface, from logic bugs to memory corruptions, with local and remote attack surface. The idea of targeting such defense mechanisms to bypass system security guarantees is deeply explored in the Antivirus Hacker's Handbook (Koret and Bachaalany 2015) where the authors outline multiple attack scenarios, vulnerability types and surfaces.
As this was a limited scope engagement we decided to only audit kernel components with the specific goal of finding local privilege escalation primitives. In order to progress efficiently we aimed to identify and focus on the attack surface that is presumably directly reachable by the user and processes user controlled data. The multitude of
calls, exposed by various kernel drivers, seemed like a promising target.IOCTL
Avast ships and installs many different kernel components. After checking which of these drivers are accessible to the user, based on their respective ACLs, we compiled a list of potential targets:
aswRvrt
aswSnx
aswSP_Handler
aswSP_Open
AswVmm
AvAswIDS_loc2
AvAswUniv
We manually checked the different devices in order to find the most promising one based on the number of exposed IOCTLs. The initial reverse engineering effort revealed a series of strings that seemed to suggest that these kernel components are shared between various Gendigital AV products (Avast, AVG, Norton and Avira). We did not verify whether this is the case and if the implementation details are the same, however, this could further increase the reach and impact of potential vulnerabilities.
After this cursory analysis our choice of target became the
device driver as its interface seemed to expose by far the highest number of IOCTL handlers to the user. This conclusion was made by observing the device ACLs and the registeredaswSnx
callback. These ACLs or "Access Control Lists", are used to specify which user is granted access to what type of resources and the operations that are permitted on a given resource. In this specific case the ACL allows access to unprivileged users to the driver. Whereas theIRP_MJ_DEVICE_CONTROL
is a request handler that is registered by the driver itself and it is responsible for handling the IOCTL requests that are sent by the user throughIRP_MJ_DEVICE_CONTROL
API and passed down by the driver stack. The driver's logic exposed through this interface seemed sufficiently complex and rich to be a good target for research. In theory, due to the ACL rules, this interface should be directly accessible by a regular user.DeviceIoControl
To this day we don't know the exact purpose of the various audited IOCTL-s in the driver. Many of them seem to be implementing wrappers around different system calls, most likely to enforce some kind of filtering or additional access control. The rest of the IOCTLs appear to be related to interacting with settings and configuration files.
Bug Discovery
During a normal audit we would spend time to understand the attack surface, reverse engineer the implementation and either follow the user provided data or look for deep bugs in how certain systems interact. On these time-constrained engagements we have to be more opportunistic as we can't afford a thorough, systematic approach. Instead, we aim to quickly identify where user data is handled and verify the parsing logic that follows the data access.
This type of approach was used for auditing the
driver as well. We combined a traditional manual reverse engineering effort with a few simple tricks to quickly identify where user data is handled in the driver. The primary goal was to follow the parsing code and find localized memory corruption vulnerabilities that stem from incorrect data handling, such as integer arithmetic issues, buffer overruns, miscalculated indexes and double fetch and TOCTOU issues. Generally, finding more complex vulnerabilities such as deep logic bugs, lock contention and resource management problems, non-trivial race conditions require a deeper understanding of the target system and the interactions between various subcomponents.aswSnx
The
function is a good indicator that user controlled data is processed by the kernel as they are used to check if a userland address is safe to access for the kernel. Furthermore, as such data resides in the user-space a malicious user process can control and change its content at any time even while the kernel is processing it. Locating these functions and their inlined versions allow us to forgo reversing and understanding all the, often complicated, preceding logic in the IOCTL handler.ProbeForRead/ProbeForWrite
With this method we quickly identified an IOCTL that checks the validity of user pointers with the
function but then it processes the data in an unsafe manner. The purpose of this suspicious IOCTLProbeForRead
is unclear to us, all we know is that it is used to process a user supplied string. While retrieving a unicode string from the user, the driver fails to capture and copy the supplied structure to kernel memory before manipulating it. When the kernel directly operates on user memory there are no guarantees about the consistency of the data, a user process can modify it at any time. This can lead to typical double fetch and time-of-check-time-of-use vulnerabilities in case the data is read multiple times and the kernel code assumes that the values remain the same.0x82AC0204
As can be seen in the following code, while trying to capture the string supplied by the user, the Length field of the
structure is fetched two times, which expose the system to a critical security issue._UNICODE_STRING
The pseudocode of the vulnerable function is presented below:
1__int64 sub_14005E99C(char a1, __int64 a2, const WCHAR *a3, __int64 a4, ...)2{3[...]4if ( !a1 && unicodestring_user )5{6ProbeForRead(unicodestring_user, 0x10, 1u); // [0]7ProbeForRead(unicodestring_user->Buffer, unicodestring_user->Length, 1u); // [1]8}9v17 = sub_140071C6C((__int64)v18, &v14[v23 + 13], unicodestring_user); // [2]10}1112__int64 __fastcall sub_140071C6C(__int64 a1, _QWORD *a2, _UNICODE_STRING *unicodestring_user)13{14PoolWithTag = ExAllocatePoolWithTag(PagedPool, unicodestring_user->Length + 16, 0x20786E53u); // [3]1516if ( !PoolWithTag )17return 0xC000009A;1819Length = unicodestring_user->Length;20*PoolWithTag = unicodestring_user->Length;21PoolWithTag[1] = Length;22*((_QWORD *)PoolWithTag + 1) = PoolWithTag + 8;23Buffer = (char *)unicodestring_user->Buffer;24if ( Buffer )25{26if ( a3->Length )27private_memmove((_OWORD *)PoolWithTag + 1, Buffer, unicodestring_user->Length); // [4]28}29*a2 = PoolWithTag;30return 0;31}
During the execution of the
subroutine the kernel usessub_14005E99C
to verify that theProbeForRead
structure [0] and the string buffer [1] resides in user-space and is safe to access. The_UNICODE_STRING
function is called [2] to copy the content of the string into kernel memory. The value of the length field is fetched twice from user memory:sub_140071C6C
First at [3] when the kernel buffer is allocated
Second at [4] when the user memory is copied to the freshly allocated kernel buffer
The problem with this sequence of events is that the kernel assumes that the length values remain the same between [3] and [4], however the system does not provide such guarantees. A malicious user-process can change the length field between the two accesses resulting in a different kernel allocation and copy size. If the allocation size is chosen to be smaller than the copy size that would lead to a kernel heap pool overflow where user supplied data is copied beyond the boundaries of the allocated kernel buffer. Please note that this is a very strong and convenient primitive as both the initial pool size, the overflow amount and the overflow data is controlled by the user.
The complete input of the vulnerable IOCTL and the definition of the
structure is presented below._UNICODE_STRING
1typedef struct _UNICODE_STRING {2USHORT Length;3USHORT MaximumLength;4DWORD Padding;5PWSTR Buffer;6} _UNICODE_STRING;78typedef struct _InputSubStruct {9wchar* pString;10DWORD Type;11char unknown[0x4];12void* pData; // wchar* or UNICODE_STRING*13} InputSubStruct;1415typedef struct _InputStruct {16char unknown[0x88];17InputSubStruct SubStructArray[0xF];18char unknown2[0x8];19} InputStruct;
Once a vulnerability is identified through static analysis the natural next step is to dynamically reproduce it. To our surprise our initial attempts to exercise the vulnerable code were an absolute failure. When we called the target IOCTL with seemingly right parameters, nothing happened, no crash, no side effects. Taking a closer look with a debugger showed that the code was not even executing.
Investigation on this issue was pointing to the sandbox configuration, the process initiating the IOCTL needs to be registered into the avast sandbox to access it, this means that a standard process can not access it by default.
Enter Sandbox
Further investigation and reversing revealed that the
driver is responsible for some kind of sandbox implementation and the vulnerable IOCTL is only accessible to the sandboxed processes. Our best guess is that the AV tries to isolate untrusted processes and limit their access to system resources. By default processes are executed outside of this sandbox and they have regular access to the system but not the sandbox specific IOCTL calls. This presented the curious case where a successful attack to reach the vulnerable code required breaking into the sandbox instead of escaping as usual. Exit light, enter night.aswSnx
The
driver has a registered callback that notifies it when a process is started. This callback is responsible for, among other things, checking if a newly created process needs to be sandboxed or not. The decision is governed by a configuration file that describes the actions that need to be applied to different processes and executables. The file is located underaswSnx
and it is protected by the aswSP.sys FS Filter driver which prevents direct access to regular users.%ProgramData%\Avast Software\Avast\snx_lconfig.xml
By default the configuration file is empty and therefore nothing is executed within the sandbox. Further understanding the
driver revealed that we would need to have a sandbox profile where theaswSnx
flag andfAutosandbox
variable are set for a target process to reach the vulnerable IOCTL. To verify this theory we manually crafted a config file that would place the attacker process in the sandbox. Fortunately a global variable is used by the filter driver to enable file protection enforcement. This variable can be modified with a kernel debugger to temporarily provide access to the file and test settings.scanhandle
An example configuration that allows access to the vulnerable attack surface:
1<?xml version="1.0" encoding="UTF-16"?>2<SnxConfig type="Configuration" version="3">3<Kernel>4<Avast>5<ProcessNameEntry flags="fLastFilenamePart|fAutosandbox" scanhandle="1" name="exploit.exe"/>6</Avast>7[...]8</Kernel>9</SnxConfig>
Modifying the configuration by hand through disabling its protection with a kernel debugger is sufficient to confirm our understanding of the driver, however it is not practical for reaching and exploiting the vulnerability. We had to go back to do some further reversing to search for APIs or some indirect behaviors that would allow the modification of the config file to spawn our process in the sandbox.
Most of the IOCTLs that are related to the configuration file are not reachable directly from an average user process. As they require the driver to be opened with write permission, however the driver itself prevents regular processes opening it for writing. Most likely the reason is to prevent unprivileged processes from removing entries from the sandbox profile. Fortunately, the IOCTL that is used to register new processes to the sandbox config and set their initial configuration is accessible with read only permissions.
The IOCTL that allow us to do this is
and its input is the following:0x82AC0054
1typedef struct _CfgInput {2UINT64 Flags;3UINT64 ScanHandle;4UINT64 EngineFlags;5wchar_t Name[0x805];6} CfgInput;
The
field needs to be set to 1, theScanHandle
field needs to be set to 0x1C and theFlags
field needs to contain the name of the executable that shall be executed within the sandbox. From there we can finally reach the target IOCTL, trigger the vulnerability and admire a magnificent bluescreen.Name
Additional Findings
As the attack surface is relatively obscure and the initial vulnerability was a quite shallow find we decided to spend a bit more time auditing the same area. Within the same IOCTL handler we found multiple variants and similar bugs, they process similar inputs with different string encodings The findings are briefly introduced below.
Another Double Fetch into a Pool Overflow
Similar to the originally introduced double fetch vulnerability when the same IOCTL processes the
input field (instead of unicode) there is another double fetch vulnerability. In this case the length is not retrieved from user-space directly, instead a loop is used to iterate the string first to calculate its length [0]. Then a kernel heap pool buffer is allocated [2] with the calculated size. Finally, in another loop the input string is iterated again to copy its content to the freshly allocated buffer.pString
In both loops the length of the string is determined by the location of the first null byte. As the string resides in user space its content, and consequently its size, can change between [0] and [3]. If the length is larger during the second iteration the pool buffer is overflown with user controlled data.
The pseudocode is presented below:
1typedef struct _InputSubStruct {2wchar* pString;3DWORD Type;4char unknown[0x4];5void* pData; // wchar* or UNICODE_STRING*6} InputSubStruct;78typedef struct _InputStruct {9char unknown[0x88];10InputSubStruct SubStructArray[0xF];11char unknown2[0x8];12} InputStruct;1314__int64 sub_14005E99C(char a1, __int64 a2, const WCHAR *a3, __int64 a4, ...) {15[...]16// [0] Calculate size of the string17size = -1;1819do {20++size;21}2223while (pString[size]);2425// [1] Buffer allocation based on the calculated size26heap_buff_p = ExAllocatePoolWithTag(PagedPool, 2 * size + 2, 0x20786E53u);27[...]2829// [3] Copy the string into the allocated buffer while fetching a second time the string30do {31v25 = *pString;32*heap_buff_p = *pString;33pString++;34++heap_buff_p;35}36while ( v25 );37[...]
Another problem with this code is that the supplied user pointer is inherently trusted. It is dereferenced directly without being verified by the appropriate
function. As a result the user can supply an invalid pointer which would crash the kernel leading to a local DoS. We found other similar missing pointer verification issues within the IOCTL handler at multiple locations.ProbeForRead
Pool Buffer Overflow During Process Termination
The same IOCTL can be used to provide a wide char string to the kernel. The exact purpose of this string is not clear, however, the kernel allocates a new buffer and stores the user provided string. Both the size and the content is controlled by the user. The string is saved for later processing in an internal structure.
When the sandboxed process is terminated it is unregistered from the sandbox. The deregistration process involves a step when the previously captured and saved, user-provided string is copied into a fixed-size kernel heap pool buffer. The root cause of the vulnerability is the misuse of the
API. These types of functions expect to receive the destination buffer size as a second parameter to prevent out of bound copies to it. The driver, however, passes the source string size (converted to regular string length from wchar length) as the copy length constraint, instead of the fixed destination buffer length. As there are no constraints on the user controlled source buffer size this could lead to a kernel heap pool overflow with user controlled data.*snprintf
The pseudocode associated with the bug is the following:
1__int64 __fastcall sub_14005A7DC(__int64 a1, __int64 a2) {2[...]3_wsnprintf(allocation_0x420, pString_copy_size >> 1, L"%ws", pString_copy);4[...]5}
The code responsible for capturing the user provided
into the kernel is the same as the previously described issue.pString
Yet Another Double Fetch into a Pool Overflow
In the same IOCTL, there is yet another double fetch issue while processing the
field from thepData
structure. Similarly to the previous case the user space string is iterated first [0] to find the NULL terminator and calculate its size. The destination kernel structure is allocated from the heap pool using this calculated size [1]. The input string is iterated again to recalculate the input string size [2] andInputSubStruct
is called at [3] to copy the user controlled data into the kernel buffer.memcpy
As previously the length and content of the user string can be altered between [0] and [3] which results in different sizes being used for the allocation and the copy.
The pseudocode associated with the bug is the following:
1typedef struct _InputSubStruct {2wchar* pString;3DWORD Type;4char unknown[0x4];5void* pData; // wchar* or UNICODE_STRING*6} InputSubStruct;78typedef struct _InputStruct {9char unknown[0x88];10InputSubStruct SubStructArray[0xF];11char unknown2[0x8];12} InputStruct;1314__int64 sub_14005E99C(char a1, __int64 a2, const WCHAR *a3, __int64 a4, ...) {15[...]1617ProbeForRead((volatile void *)pData, 2ui64, 1u);1819// [0] Calculate allocation size20if ( pData ) {21i = -1i64;22do23++i;24while (pData[i]);25allocation_size = 2 * i;26}2728else {29allocation_size = 0;30}3132v38 = &v14[v23];33// [1] Kernel heap buffer allocation34v17 = alloc_buffer(1i64, &v38->dst_struct, allocation_size);35if ( v17 < 0 )36goto LABEL_102;37// [2] Calculate copy size38if ( pData ) {39j = -1i64;40do41++j;42while (pData[j]);43// Copy size assignment44v38->dst_struct->length = 2 * j;45[...]46// [3] memcpy using the recalculated size47memcpy(v38->dst_struct->data, (char *)pData, v38->dst_struct->length);48[...]49}50}
Patches
We have done a very brief patch analysis to verify if the vulnerabilities were correctly fixed before publishing. The first double fetch vulnerability is fixed by copying the unicode description structure into the kernel before processing. This prevents further access to a malicious user-space process, eliminating the vulnerability.
The other double fetch vulnerabilities are addressed by removing the second loop that recalculates the length of the string. Instead the result of the original iteration is used to make the allocation and the copy.
The pool overflow on process termination has been fixed by verifying the size of the user supplied string against the fixed allocation size before the copy.
Finally, new validity checks have been added before user addresses are accessed and dereferenced in the kernel driver.
Conclusion
We spent around a month on this project and during that time we found four distinct kernel heap overflow vulnerabilities and two system dos vulnerabilities in a widely deployed target and we successfully exploited them for system privilege escalation. Reaching the vulnerabilities required to solve some unique challenges that were raised by the sandboxing model of the target application. After further investigation and understanding the sandbox we found a way to modify its configuration, spawn a controlled process within it and reach and exploit the vulnerable attack surface. This work proves that it is still possible to find high impact vulnerabilities in established, widely used software with limited resources, relying on manual code auditing, using opportunistic heuristics.
The next part of this blog post series will contain all the exploitation details.
Timeline
25-03-14: Vulnerability reports submitted to Bugcrowd platform (as per vendor instructions)
25-03-18: All submissions (except one of the DoS) are marked as a duplicate of each other by the vendor
25-03-27: Further explanation and evidence is provided that the submissions are variants and not duplicates
25-04-01: Updated version of Avast is released fixing all but one of the heap overflow vulnerabilities
25-05-26: Vendor informs us of the patched version
25-05-28: Bounty is awarded for the submission
25-06-05: Patches are confirmed and vendor is alerted of the one remaining vulnerability
25-06-11: Due to the vendor request further technical details are provided
25-08-29: A status update is requested by us
25-10-02: Vendor confirms that they can no longer reproduce the vulnerability
25-10-03: We confirm that the last vulnerability has been fixed
25-11-11: CVE-2025-13032 is assigned and published
We would like to thank the Avast and Bugcrowd security team for coordinating with us in the disclosure process and fixing the vulnerabilities.
It is our opinion that eagerly classifying vulnerability variants as duplicates is not a pragmatic approach as it could dissuade researchers from reporting multiple vulnerabilities at the same time and miss-classifications can extend the patching time.
We also applaud the Avast security team for their incredibly fast patch response, it took 12 days from initial acceptance to release a fixed version of the product which is above and beyond industry standard patch cycle times.
Additional Notes
The pseudocode references are taken from
version.Avast 25.2.9898.0
Related posts
More content you might like