【问题标题】:Why does this x86 shellcode run successfully but then trigger a violation executing location?为什么这个 x86 shellcode 运行成功但随后触发了违规执行位置?
【发布时间】:2021-11-02 01:36:25
【问题描述】:

问题:为什么程序在shellcode成功完成目标后会抛出一个违反执行位置异常?

说明:我的目标是使用调用 Windows API 函数的 x86 shellcode 将 DLL 加载和卸载到当前程序中。虽然程序成功地完成了这个目标,但 Visual Studio 然后告诉我执行某个位置时存在违规行为。我知道程序成功执行,因为测试 DLL 文件在附加和分离时打印。另一个需要注意的重要细节是这仅在调用卸载函数时发生,加载函数绝对没有问题。 (如果这很重要,我将在 Visual Studio 2019 的 Windows 10 上使用 C++20 执行此操作)

我知道 shellcode 没有正确设置堆栈帧,但在将执行返回到被调用函数之前,我确保 ESP 已恢复正常。我保存了 EAX 并在卸载功能中将其恢复正常。我制作了这个测试程序,最终目标是生成可用于我正在开发的 dll 注入程序中的远程线程上下文修补方法的 shellcode。我还多次验证了用于查找返回地址的偏移量。感谢您的帮助,谢谢!

这是控制台输出。

附上! DLLMain 位于 0x79EF134D
分离!

这是抛出的异常。

在 Shellcode DLL 中的 0x9269D814 处引发异常
Loading.exe: 0xC0000005: 访问冲突正在执行
位置 0x9269D814。

这是主文件,它只有大约 120 行。

const dword follow_relative_jump(const pbyte pointer)
{
    if (pointer)
    {
        if (pointer[0] == 0xE9 || pointer[0] == 0xEB)
        {
            return reinterpret_cast<dword>(pointer + 5 + reinterpret_cast<psdword>(pointer + 1)[0]);
        }
    }


    return reinterpret_cast<dword>(pointer);
}

void load_dll(const dword path_address)
{
    /*
        68 90 90 90 90      -> push 0x????????      (return address buffer)

        68 90 90 90 90      -> push 0x????????      (LoadLibraryA() address buffer)
        68 90 90 90 90      -> push 0x????????      (DLL path address buffer)
        FF 54 24 04         -> call [esp + 4]       (calling LoadLibraryA())
        83 C4 08            -> add esp, 8           (cleaning up the stack, except for return address)

        C3                  -> ret                  (return to return address that was pushed first, it should pop it off the stack and return ESP to normal)
    */

    std::vector<byte> shellcode = {
        0x68, 0x90, 0x90, 0x90, 0x90,
        0x68, 0x90, 0x90, 0x90, 0x90,
        0x68, 0x90, 0x90, 0x90, 0x90,
        0xFF, 0x54, 0x24, 0x04,
        0x83, 0xC4, 0x08,
        0xC3
    };


    // Offset is the distance from the function prologue to the next instruction after the call to load_dll()
    reinterpret_cast<pdword>(shellcode.data() + 1)[0] = follow_relative_jump(reinterpret_cast<pbyte>(&load_dll)) + 0x22C;
    reinterpret_cast<pdword>(shellcode.data() + 6)[0] = reinterpret_cast<dword>(&LoadLibraryA);
    reinterpret_cast<pdword>(shellcode.data() + 11)[0] = path_address;



    if (const auto allocation = VirtualAlloc(NULL, shellcode.size(), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE))
    {
        memcpy(allocation, shellcode.data(), shellcode.size());
        reinterpret_cast<void(__cdecl*)()>(allocation)();
        VirtualFree(allocation, shellcode.size(), MEM_FREE);
    }
}

void unload_dll(const dword path_address)
{
    /*
        68 90 90 90 90      -> push 0x????????      (return address buffer)
        50                  -> push eax             (save EAX so we can set it back later)

        68 90 90 90 90      -> push 0x????????      (GetModuleHandleA() address buffer)
        68 90 90 90 90      -> push 0x????????      (DLL path address buffer)
        FF 54 24 04         -> call [esp + 4]       (calling GetModuleHandleA())
        83 C4 08            -> add esp, 8           (clean up the stack, except for return address and saved EAX)

        68 90 90 90 90      -> push 0x????????      (FreeLibrary() address buffer)
        50                  -> push eax             (Handle to module returned by GetModuleHandleA() in EAX)
        FF 54 24 04         -> call [esp + 4]       (calling FreeLibrary())
        83 C4 08            -> add esp, 8           (clean up stack, except for return address and saved EAX)

        58                  -> pop eax              (set back EAX to what it was before)
        C3                  -> ret                  (return to return address that was pushed first, it should pop it off the stack and return ESP to normal)
    */

    std::vector<byte> shellcode = {
        0x68, 0x90, 0x90, 0x90, 0x90,
        0x50,
        0x68, 0x90, 0x90, 0x90, 0x90,
        0x68, 0x90, 0x90, 0x90, 0x90,
        0xFF, 0x54, 0x24, 0x04,
        0x83, 0xC4, 0x08,
        0x68, 0x90, 0x90, 0x90, 0x90,
        0x50,
        0xFF, 0x54, 0x24, 0x04,
        0x83, 0xC4, 0x08,
        0x58,
        0xC3
    };


    // Offset is the distance from the function prologue to the next instruction after the call to unload_dll()
    reinterpret_cast<pdword>(shellcode.data() + 1)[0] = follow_relative_jump(reinterpret_cast<pbyte>(&unload_dll)) + 0x2AF;
    reinterpret_cast<pdword>(shellcode.data() + 7)[0] = reinterpret_cast<dword>(&GetModuleHandleA);
    reinterpret_cast<pdword>(shellcode.data() + 12)[0] = path_address;
    reinterpret_cast<pdword>(shellcode.data() + 24)[0] = reinterpret_cast<dword>(&FreeLibrary);



    if (const auto allocation = VirtualAlloc(NULL, shellcode.size(), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE))
    {
        memcpy(allocation, shellcode.data(), shellcode.size());
        reinterpret_cast<void(__cdecl*)()>(allocation)();
        VirtualFree(allocation, shellcode.size(), MEM_FREE);
    }
}

int main()
{
    const char* path = "C:\\Users\\maxbd\\Desktop\\test.dll";


    load_dll(reinterpret_cast<dword>(path));
    unload_dll(reinterpret_cast<dword>(path));


    static_cast<void>(std::getchar());
    return 0;
}

【问题讨论】:

  • LoadLibraryAstdcall 意味着它会删除参数本身,你不应该这样做。
  • 哦,我不知道。谢谢,我回家看看能不能解决。

标签: c++ windows assembly x86 shellcode


【解决方案1】:

我没有考虑我试图调用的函数的调用约定以及它们应该如何操作。 Windows API 函数使用__stdcall 将函数参数从函数内的堆栈中弹出。所以我应该只弹出我推送的函数地址而不是函数参数。感谢您在评论中提供的信息,Jester。

另外,我必须将0xC3 返回指令更改为0xC2 0x04 0x00,以便它将返回地址从堆栈中弹出。我认为正常的0xC3 返回会为我做到这一点,但显然它没有。或者至少在这种情况下由于某种原因它不会。如果我不手动弹出它,Visual Studio 会抛出一个关于 ESP 不正确的异常。如果我这样做了,它在加载和卸载 DLL 时都能完美运行。

我也完全忘记了,因为这是一个测试程序,所以我使用函数指针将 shellcode 作为__cdecl 函数调用,而不是劫持远程线程的执行和修改 EIP,因此 call 指令正在使用所以我没有理由手动推送返回地址。 我假设我自愿未能正确设置堆栈帧是返回行为异常的原因,因为返回地址应该高于 EBP。 因为使用了call,所以返回地址被推送了两次,所以需要在返回后弹出一个 dword 的返回指令来摆脱自动推送的返回地址。当我将 shellcode 应用到我的实际程序中时,我会尝试相对跳转而不是返回,这在这种情况下更有意义并且更简洁。

如果我误解了这个解决方案,我不会感到惊讶,但它似乎有效,所以我会认为它已解决。

【讨论】:

  • C3 ret 确实从堆栈中弹出返回地址。如果你的调用者希望你弹出一个arg,你只需要ret 4,也就是说,它假设这个函数也是stdcall或另一个callee-pops约定,只有一个arg。这很可能是 32 位代码的默认设置。
  • 我认为这与我没有设置基指针以使返回地址在其上方有关,但我会保持这样,除非我发现它会在更多测试中引起问题。
  • ret 与 EBP 的交互为零。当 ret 执行时,您需要 ESP 指向返回地址。之后,ESP 指向它上面(如果你使用 C2 ret imm16 可能会更高)。您的调用者期望 ESP 具有与进入时相同的值,或者对于 callee-pops 约定高 n * 4 个字节。反汇编调用函数应该清楚这一点,尽管即使对于 caller-pops 可能会推迟弹出以重复使用相同的 arg 空间进行多次调用。
  • 在阅读您更新的答案后,是的,如果执行通过劫持跳转或 ret 到达您的块,那么您可能在进入“函数”时在堆栈上没有返回地址。在这种情况下是的,如果您注入的代码与目标代码的距离已知(没有 ASLR,或在静态缓冲区中),则相对跳转是有意义的。是的,您可能需要调整堆栈以匹配您“返回”到的位置与您来自的位置。
  • 哦,好的,谢谢。所以我假设在这种情况下发生的事情是我的返回指令实际上是使用我手动推送的返回地址,然后弹出四个字节,这将是通过调用推送的返回地址,这将 ESP 设置回以前的状态。还有为什么我之前在使用原始加载函数时没有遇到任何问题,尽管它从堆栈中弹出了两个多 dword。
猜你喜欢
  • 2022-01-02
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2022-06-14
相关资源
最近更新 更多