See the question and my original answer on StackOverflow

STRING TABLE resources are somewhat documented here: https://devblogs.microsoft.com/oldnewthing/20040130-00/?p=40813

(PS: here's a link to the mentioned KB article Q196774 : https://www.betaarchive.com/wiki/index.php/Microsoft_KB_Archive/196774)

The strings listed in the *.rc file are grouped together in bundles of sixteen. So the first bundle contains strings 0 through 15, the second bundle contains strings 16 through 31, and so on. In general, bundle N contains strings (N-1)*16 through (N-1)*16+15.

The strings in each bundle are stored as counted UNICODE strings, not null-terminated strings. If there are gaps in the numbering, null strings are used. So for example if your string table had only strings 16 and 31, there would be one bundle (number 2), which consists of string 16, fourteen null strings, then string 31.

Here is some sample code that dumps all string resources from a given file:

void DumpStringTable(LPCWSTR filePath)
{
    HMODULE hModule = LoadLibraryEx(filePath, nullptr, LOAD_LIBRARY_AS_DATAFILE);
    if (!hModule)
    {
        wprintf(L"LoadLibraryEx failed err: %u\n", GetLastError());
        return;
    }

    if (!EnumResourceTypesEx(hModule, EnumResType, 0, 0, 0))
    {
        wprintf(L"EnumResourceTypesEx failed err: %u\n", GetLastError());
        return;
    }

    FreeLibrary(hModule);
}

BOOL EnumresLang(HMODULE hModule, LPCWSTR lpType, LPCWSTR lpName, WORD wLanguage, LONG_PTR lParam)
{
    if (lpType == RT_STRING)
    {
        const HRSRC res = FindResourceEx(hModule, lpType, lpName, wLanguage);
        if (!res)
        {
            wprintf(L"FindResourceEx failed err: %u\n", GetLastError());
            return FALSE;
        }

        const DWORD size = SizeofResource(hModule, res);
        if (!size)
        {
            wprintf(L"SizeofResource failed err: %u\n", GetLastError());
            return FALSE;
        }

        HGLOBAL hMem = LoadResource(hModule, res);
        if (!hMem)
        {
            wprintf(L"LoadResource failed err: %u\n", GetLastError());
            return FALSE;
        }

        LPWORD data = (LPWORD)LockResource(hMem);
        if (!data)
        {
            wprintf(L"LockResource failed err: %u\n", GetLastError());
            return FALSE;
        }

        const WORD nameInt = (WORD)(((ULONG_PTR)lpName) & 0xFFFF);
        for (WORD i = 0; i < 16; i++)
        {
            const WORD len = *data;
            data++;
            if (len)
            {
                const WORD id = (nameInt - 1) * 16 + i;
                std::wstring str;
                str.append((const wchar_t*)data, len);
                data += len;
                wprintf(L"id:%u: %s\n", id, str.c_str());
            }
        }

        return TRUE;
    }
    return TRUE;
}

BOOL EnumResName(HMODULE hModule, LPCWSTR lpType, LPWSTR lpName, LONG_PTR lParam)
{
    if (!EnumResourceLanguagesEx(hModule, lpType, lpName, EnumresLang, 0, 0, 0))
    {
        wprintf(L"EnumResourceLanguagesEx failed err: %u\n", GetLastError());
        return FALSE;
    }

    return TRUE;
}

BOOL EnumResType(HMODULE hModule, LPWSTR lpType, LONG_PTR lParam)
{
    if (!EnumResourceNamesEx(hModule, lpType, EnumResName, 0, 0, 0))
    {
        wprintf(L"EnumResourceNamesEx failed err: %u\n", GetLastError());
        return FALSE;
    }

    return TRUE;
}