A quick look at the bug

CVE-2024-41592 is one of the issues discussed in the DrayTek router research released by Forescout under the title Breaking Into DrayTek Routers Before Threat Actors Do It Again. The vulnerable code sits in GetCGI(), where malformed handling of string parameters can run past bounds and corrupt the stack.

image

The key point is simple: when the function parses QUERY_STRING, too many &-separated parameters cause a stack-based array of pointers to overflow. Under the right conditions, that overwrite can reach saved control data, including the return address.

Firmware setup and debugging environment

The analysis here uses DrayTek 3910 firmware version 4.3.1 as the target for debugging and testing. The details of firmware decryption and unpacking are not repeated here, but prior public research on DrayTek emulation covers that process well.

After extraction, the main program can be found at:

rootfs/firmware/vqemu/sohod64.bin

The DrayTek 3910 uses an unusual architecture: an ARM Linux system runs QEMU, and QEMU in turn runs the DrayOS RTOS. For debugging, one workable approach is to build DrayTek’s open-source QEMU code and use that environment directly.

Before starting, firmware/setup_qemu_linux.sh and run_linux.sh need a few changes. For example, adding -s to qemu-system-aarch64 in run_linux.sh makes remote debugging much easier.

image

Finding the vulnerable path

A signed DrayTek 2830 firmware image can be used to quickly identify the GetCGI() function in DrayTek 3910 version 4.3.1. Another straightforward route is to cross-reference the QUERY_STRING string.

image

Various CGI handlers call GetCGI() to parse incoming parameters.

image

Inside GetCGI(), each time an & is encountered, the code uses makeword to allocate memory for one parameter and stores the resulting pointer on the stack. The relevant logic looks like this:

<table> <thead> <tr> <th>1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27</th> <th>v19 = sub_400BFA18("REQUEST_METHOD", a3); if ( v19 ) { if ( !strcmp(v19, "GET") ) { v18 = sub_400BFA18("QUERY_STRING", a3); if ( !v18 ) return 0; idx = 0; while ( *v18 ) { *(a2 + 8 * idx) = makeword(v18, '&'); // overflow plustospace(*(a2 + 8 * idx)); unescape_url(*(a2 + 8 * idx)); v16 = safe_strcrh(*(a2 + 8 * idx), '='); if ( v16 ) { *v16 = 0; *(a2 + 8 * idx + 4LL) = v16 + 1; } else { *(a2 + 8 * idx + 4LL) = 0; } ++idx; } }</th> </tr> </thead> <tbody> <tr> <td></td> <td></td> </tr> </tbody> </table>

The write target here, (a2 + 8 * idx), lives on the stack. If the request contains enough & separators, the repeated pointer writes run off the intended region and start smashing adjacent stack variables.

image

At that point, the overflow is no longer limited to local state. A sufficiently long parameter list can overwrite a large number of stack pointers and eventually reach the saved return address.

Why exploitation is not entirely straightforward

Overwriting the return address in GetCGI() is only part of the problem. When many CGI handlers finish, they call FreeCtrlName(). That cleanup routine walks through the request data structures and zeroes out pointers that were stored on the stack.

image

This is the main obstacle during exploitation: the overwritten return-address area may not survive cleanup intact.

The logic of FreeCtrlName() is shown below:

<table> <thead> <tr> <th>1 2 3 4 5 6 7 8 9 10 11 12 13</th> <th>__int64 __fastcall FreeCtrlName(__int64 result) { int v1; // [xsp+1Ch] [xbp+1Ch] int i; // [xsp+2Ch] [xbp+2Ch] v1 = result; for ( i = 0; *(v1 + 8 * i); ++i ) { result = sub_4061D7CC(*(v1 + 8 * i), 0x154u); *(v1 + 8 * i) = 0; } return result; }</th> </tr> </thead> <tbody> <tr> <td></td> <td></td> </tr> </tbody> </table>

The function repeatedly frees pointers from the stack until it encounters a zero entry. Because of that behavior, exploitation needs a way to place a 0 at the right spot on the stack so the cleanup loop stops before destroying the useful overwrite.

An important detail from the public research is that the lower 4-byte pointer slots are freed and nulled, while the higher 4-byte entries—such as pointers to parameter values—are oddly left alone. That asymmetry is what makes the bypass possible.

Reaching a usable CGI target

The public write-ups and later Black Hat EU slides mention a [vulnerable-cgi-page].cgi, but do not spell out which CGI endpoint this actually is.

image

A practical way to locate it is fairly direct:

  1. Enumerate the functions that invoke CGI handlers.
  2. Filter for handlers that do not require authentication.
  3. Look for functions whose parameter processing can write a zero value onto the stack.

A rough heuristic for step 2 is that handlers without a call resembling CGIbyFieldName = GetCGIbyFieldName(v6 + 32, "sFormAuthStr"); do not require authentication.

For step 3, one obvious place to look is code paths such as atoi(query_string), where attacker-supplied HTTP parameters influence integer values and can potentially result in a stack write of 0.

Using this approach, it is possible to find a CGI endpoint that is both unauthenticated and capable of producing the zero needed to stop FreeCtrlName() at the right moment.

From stack corruption to command execution

Once that cleanup problem is bypassed, the overflow can be used to redirect control flow to an address whose contents are fully controlled through request parameters. Because the program is running inside a QEMU-based environment, an attacker can place arbitrary shellcode at the target address.

The final step is escaping beyond the guest context. The program exposes a function named virtcons_out, which can trigger special commands. By injecting into its first argument, it becomes possible to execute arbitrary commands on the host side.

image

That turns what starts as a parameter-parsing bug in GetCGI() into a path toward full code execution.

References

  1. Breaking Into DrayTek Routers Before Threat Actors Do It Again
    https://www.forescout.com/resources/draybreak-draytek-research/

  2. HEXACON2022 - Emulate it until you make it! Pwning a DrayTek Router by Philippe Laulheret
    https://www.youtube.com/watch?v=CD8HfjdDeuM

  3. When (Remote) Shells Fall Into The Same Hole: Rooting DrayTek Routers Before Attackers Can Do It Again
    https://i.blackhat.com/EU-24/Presentations/EU24-Dashevskyi-When-Remote-Shells-Fall-Into-The-Same-Hole.pdf