// // dumpster_ordinals.cpp // https://pastebin.com/C3qcNMbh // // See Homies Twee t relating to cli obfus in Windows /* https://x.com/sixtyvividtails/status/1795410561775095816 https://x.com/sixtyvividtails/status/1797667842185372084 https://x.com/sixtyvividtails/status/1825951974153662711 https://x.com/malmoeb/status/1825888362265182578 */ // Compile via msvc: // cl /O2 /GS- /Brepro dumpster_ordinals.cpp /link /dll /nocoffgrpinfo /noentry /out:dumpster_ordinals.dll // // Self-test via rundll32: // rundll32.exe %__CD__%\dumpster_ordinals.dll,test // #define _NO_CRT_STDIO_INLINE #include #include #pragma comment(lib, "ntdllp") // you might need wdk for this #pragma comment(lib, "user32") #pragma comment(lib, "kernel32") #pragma execution_character_set("utf-8") // round 1 #pragma comment(linker, "/export:A=advapi32.dll.#1212,@1") // with dll extension (and we enforce our ordinal) #pragma comment(linker, "/export:B=advapi32.#+4294968508!") // overflows uint32 into 1212 // "advapi32.#▲ ♫1212*1337", linker directive quoted due to " "; RtlCharToInteger skips leading chars with ords <= 0x20 #pragma comment(linker, "\"/export:C=advapi32.#\x1E \x0E" "1212*1337\"") #pragma comment(linker, "/export:D=advapi32.#-0x44FFFFFB44") // overflows and then gets negated // round 2 #pragma comment(linker, "/export:E=advapi32.dll::$DATA.#1212") // use ads streams (here unnamed default) #pragma comment(linker, "/export:F=C:\\windows\\system32\\advapi32.#1212") // full path #pragma comment(linker, "/export:G=\\windows\\system32\\advapi32.#1212") // ok only when current drive is C: #pragma comment(linker, "/export:H=C:advapi32.#1212") // 👎 doesn't work even if cwd is %system32% // round 2 with editing own export (only coz msvc linker doesn't allow to build such file statically) #pragma comment(linker, "/export:J=dumpster_ordinals.#") // 👎 means ordinal is 0; but 0 ordinal doesn't work #pragma comment(linker, "/export:K=dumpster_ordinals.#0x13370001") // not an overflow(!): ok when ordinalBase 0x13370001 // for rundll32.exe test #pragma comment(linker, "/export:test") #pragma comment(linker, "/alternatename:test=_test@16") // needed for x32 only extern "C" IMAGE_DOS_HEADER __ImageBase; // linker-defined static void test_round2() { HMODULE ownImage = (HMODULE)&__ImageBase; char funcName[2]{"E"}; void* funcs[6]; for (int i = 0; i < _countof(funcs); ++i, ++funcName[0]) funcs[i] = GetProcAddress(ownImage, funcName); // prepare to edit our own export table, to change ordinal base first to 0, and then to 0x13370001; // it doesn't have to be edited dynamically, it's just msvc linker doesn't allow us to build it statically auto* peHeader = (const IMAGE_NT_HEADERS*)(PBYTE(ownImage) + __ImageBase.e_lfanew); UINT exportRva = peHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress; auto* exports = (IMAGE_EXPORT_DIRECTORY*)(PBYTE(ownImage) + exportRva); ULONG dummyOldProtect; VirtualProtect(exports, sizeof(*exports), PAGE_READWRITE, &dummyOldProtect); UINT origOrdinalBase = exports->Base; void* funcsX[2]; exports->Base = 0; // 'J' still won't resolve, but only because LdrpGetProcedureAddress bans ordinal 0 funcsX[0] = GetProcAddress(ownImage, "J"); exports->Base = 0x13370001; // 'K' should resolve now funcsX[1] = GetProcAddress(ownImage, "K"); // note if module has OrdinalBase larger than 0xFFFF in its export table, you won't be able to resolve any of its // funcs by ordinals via GetProcAddress; however, you can use native ntdll!LdrGetProcedureAddressForCaller for that exports->Base = origOrdinalBase; wchar_t buf[0x740]; swprintf_s(buf, _countof(buf), LR"(Secret round2 discovered! %p "E" > advapi32.dll::$DATA.#1212 %p "F" > C:\windows\system32\advapi32.#1212 %p "G" > \windows\system32\advapi32.#1212 should work iff "C:" is current drive %p "H" > C:advapi32.#1212 does NOT work even if %%system32%% is cwd %p "J" > dumpster_ordinals.# %p "J" > dumpster_ordinals.# ^ OrdinalBase is 0 - still does NOT work %p "K" > dumpster_ordinals.#0x13370001 %p "K" > dumpster_ordinals.#0x13370001 ^ OrdinalBase is 0x13370001 - should work okay)", funcs[0], funcs[1], funcs[2], funcs[3], funcs[4], funcsX[0], funcs[5], funcsX[1]); MessageBoxW({}, buf, L"dumpster ordinals", MB_OK|MB_ICONINFORMATION|MB_SETFOREGROUND); } extern "C" void __stdcall test(HWND, HINSTANCE, LPSTR, int) { void* funcs[4]; char funcName[2]{"A"}; unsigned exportsResolved = 0; for (int i = 0; i < _countof(funcs); exportsResolved += !!funcs[i], ++i, ++funcName[0]) funcs[i] = GetProcAddress((HMODULE)&__ImageBase, funcName); // may only retrieve image base like that once we've resolved forwarded exports HMODULE advapi = GetModuleHandleA("advapi32"); void* testFuncs[3]{}; if (advapi) { testFuncs[0] = GetProcAddress(advapi, "CryptGenRandom"); testFuncs[1] = GetProcAddress(advapi, (LPCSTR)1212); testFuncs[2] = GetProcAddress(advapi, (LPCSTR)1213); } wchar_t buf[0x400]; swprintf_s(buf, _countof(buf), LR"(%p advapi32.dll %p dumpster_ordinals.dll ------------------------------------------------ Sanity test; exports resolved via GetProcAddress(advapi32): %p "CryptGenRandom" %p #1212 %p #1213 ------------------------------------------------ ------------------------------------------------ Actual test; exports resolved via GetProcAddress(dumpster_ordinals): %p "A" > advapi32.dll.#1212 %p "B" > advapi32.#+4294968508! %p "C" > advapi32.#▲ ♫1212*1337 %p "D" > advapi32.#-0x44FFFFFB44 ------------------------------------------------ RESULT: %u/4 weird exports resolved.)", advapi, &__ImageBase, testFuncs[0], testFuncs[1], testFuncs[2], funcs[0], funcs[1], funcs[2], funcs[3], exportsResolved); // Note we cheat a bit with visual representation of variant C - we use higher Unicode chars. That doesn't affect // our export table, sequence "\x1E\x20\x0E" there still consists of just 3 bytes. !▲ ♫! << Unicode // This comment line contains actual lower ansi chars just to test your environment. ! ! << Ansi int rez = MessageBoxW({}, buf, L"dumpster ordinals", MB_CANCELTRYCONTINUE|MB_DEFBUTTON2|MB_ICONINFORMATION|MB_SETFOREGROUND); if (rez == IDTRYAGAIN) test_round2(); }