shellcode是一段能够完成某种特定功能的二进制代码,可能是开启一个新的shell或者下载某个特定的程序或者向攻击者返回一个shell等等。在Windows中,完成这些特定功能需要调用某些API,其入口地址可由链接库的装载基地址和API在链接库中的偏移相加得到。早些时候,可以这样来获取API的入口地址:进入VC6.0安装目录下的Tools文件夹,其中Depends.exe工具可以用来查看PE文件用到的所有链接库的装载基址,以及每个链接库的所有导出函数及偏移地址。
但是,操作系统版本号和补丁版本号开始影响动态链接库的加载地址,而且不同版本的链接库中API的偏移量也不尽相同,因此上述方法计算得到的API入口地址无法在各种操作系统版本上通用。
借助LoadLibraryA()和GetProcAddress()可以实现API入口地址的动态定位。
我们知道,所有win32程序都会加载ntdll.dll和kernel32.dll这两个最基础的动态链接库,而LoadLibraryA()和GetProcAddress()恰好就在kernel32.dll的导出表中,只要能获得这两个API的入口地址,就可以加载其他dll文件并获取其中的API。
具体定位过程:
- 通过段寄存器FS在内存中找到当前的线程环境块TEB。
- 线程环境块偏移0x30存放着指向进程环境块PEB的指针。
- 进程环境块偏移0x0C存放着指向PEB_LDR_DATA结构体的指针,其中存放着进程装载动态链接库的基本信息。
- PEB_LDR_DATA结构体偏移0x1C存放着模块初始化链表的头指针InInitializationOrderModuleList。
- 模块初始化链表InInitializationOrderModuleList按照模块的初始化顺序排列,每个结点都指向一个模块。
- 找到某个模块对应的结点后,在其基础上再偏移0x08就是该模块在内存中的基地址。
- 模块基地址偏移0x3C是该模块的PE头。
- PE头偏移0x78存放着指向函数导出表的指针。导出表偏移0x1C处的指针指向地址列表,列表中存储了导出函数的偏移地址;导出表偏移0x20处的指针指向名称列表,列表中存储了导出函数的函数名。地址列表与名称列表中的元素一一对应。
- 在名称列表中定位到所需API是第几个,然后在地址列表中找到相应的RVA,将RVA与之前获得的模块基地址相加,就是所需API在内存中的虚拟地址。
下面的shellcode就使用了动态定位API的方法,功能是执行MessageBoxA(0, “4321”, “4321”, 0)弹出窗口,最后执行ExitProcess(0)安全退出。代码下载地址:popMessageBox.cpp
/* In Visual Studio: Project/Property/Linker/Advanced/Data Execution Prevention(DEP) -> NO */ #include <windows.h> #include <stdio.h> void popMessageBox() { __asm { jmp start find_function: push ebp mov ebp,esp mov eax,fs:[0x30] // get the pointer to PEB mov eax,[eax+0x0c] // get PEB->ldr mov eax,[eax+0x14] // get PEB->ldr.InMemoryOrderModuleList.Flink module_loop: mov eax,[eax] // skip the 1st entry, or get the next entry mov esi,[eax+0x28] // LDR_MODULE->BaseDllName cmp byte ptr [esi+0x0c],'3' // judge if the 7th char of the module is '3' jne module_loop // find kernel32.dll module mov eax,[eax+0x10] // LDR_MODULE->BaseAddress mov edi,eax add edi,[edi+0x3c] // IMAGE_DOS_HEADER->e_lfanew: the PE Header mov edi,[edi+0x78] // IMAGE_NT_HEADERS->OptinalHeader.DataDirectory[0].VirtualAddress add edi,eax // the address of Export Table mov ebx,edi mov edi,[ebx+0x20] // IMAGE_EXPORT_DESCRIPTOR->AddressOfNames add edi,eax // the address of Names xor ecx,ecx // NameOrdinals name_loop: mov esi,[edi+ecx*4] add esi,eax // the address of Name[ecx] inc ecx mov edx,[esp+8] // The first half of the parameter cmp dword ptr [esi],edx jne name_loop mov edx,[esp+0xc] // The second half of the parameter cmp dword ptr [esi+4],edx jne name_loop mov edi,[ebx+0x24] // IMAGE_EXPORT_DESCRIPTOR->AddressOfNameOrdinals add edi,eax // the address of NameOrdinals mov ecx,[edi+ecx*2] // NameOrdinals[ecx] and ecx,0xFFFF // just use its lower 16-bits mov edi,[ebx+0x1c] // IMAGE_EXPORT_DESCRIPTOR->AddressOfFunctions add edi,eax // the address of Functions dec ecx sal ecx,2 mov edi,[edi+ecx] // Functions[ecx] add eax,edi // the address of the function we need pop ebp ret start: push 0x41636f72 // "rocA" push 0x50746547 // "GetP" call find_function // Get GetProcAddress's address, __cdecl add esp,8 // pop "GetProcA", clear parameters we push in stack push eax // store GetProcAddress() in stack push 0x7262694c // "Libr" push 0x64616f4c // "Load" call find_function // Get LoadLibraryA's address, __cdecl add esp,8 // pop "LoadLibr" push eax // store LoadLibraryA() in stack push 0x3233 // "32" push 0x72657375 // "user" push esp // lpFileName="user32" call eax // LoadLibraryA("user32.dll"), __stdcall add esp,8 // pop "user32" push 0x41786f // "oxA" push 0x42656761 // "ageB" push 0x7373654d // "Mess" push esp // lpProcName="MessageBoxA" push eax // hModule call [esp+0x18] // GetProcAddress(hModule,"MessageBoxA"), __stdcall add esp,0xC // pop "MessageBoxA" xor edi,edi push edi // "\0" push 0x31323334 // "4321" mov ecx,esp push edi push ecx push ecx push edi call eax // MessagBoxA(0,"4321","4321",0), __stdcall add esp,8 // pop "4321\0" add esp,8 // pop GetProcAddress() and LoadLibrary() in stack push 0x636f7250 // "Proc" push 0x74697845 // "Exit" call find_function // Get ExitProcess's address, __cdecl add esp,8 // pop "ExitProc" xor edi,edi; push edi; call eax; // ExitProcess(0) } } int main() { popMessageBox(); return 0; }