windows:shellcode 原理
Posted 第七子007
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了windows:shellcode 原理相关的知识,希望对你有一定的参考价值。
shellcode,一段短小精干的代码,放在任何地方都能执行,不依赖当前所处环境,那么就有这么几点要求:
- 不能有全局变量:函数里的局部变量在栈空间,地址是执行的时候动态分配的;但全局变量在编译时,会由编译器分配好固定的存储空间。编写shellcode的程序肯定会为这个全局变量预留空间,但执行shellcode的目标进程未必会预留,可能已经被其他全局变量占用;
- 不能有字符串:和上个类似,字符串会被编译器放在文字常量区,地址也是编译时固定写死的,目标进程的同一地址可能已经被占用,导致shellcode出错;
- 能动态获取所需系统API的地址,代码才能不写死;
针对以上要求,解决的思路:
- 所有变量都在函数内,写成局部变量,地址就能动态分配了;
- 字符串用字符数组替代,也就变成了局部变量,地址也能动态分配,比如 char *s = "hello" 可以改成char q1[] = {\'h\',\'e\',\'l\',\'l\',\'o\',\'\\0\'},q1的地址也会在栈的局部空间分配;
- 最难的就是这个:动态获取所需系统API地址;shellcode的很多操作涉及系统底层,必然调用windows的API。正常情况下,windows针对3环用户态程序提供LoadLibrary加载dll,返回dll的基址。然后通过GetProcAddress从dll种获取函数基址;但这两个函数本身也是windwos 的API,其地址依然需要动态获取,该怎么做了?
1、动态获取LoadLibraryA的地址
windwos 32位下,每个进程都有PEB结构体,记录了进程各种信息;在0xc处是PEB_LDR_DATA结构体,该结构体记录了顺序加载的模块链表,说明如下:
核心代码如下:
(1)找到链表的头指针:(注意:不同版本windows的PEB结构体可能有细微差别,需要适配)
__asm{ mov eax,fs:[0x30] mov eax,[eax+0x0c] add eax,0x0c mov pBeg,eax mov eax,[eax] mov pPLD,eax }
(2)遍历链表,查找kernerl32.dll
while(pPLD!=pBeg) { pLast=(WORD*)pPLD->BaseDllName.Buffer; pFirst=(WORD*)szKernel32; while(*pFirst && (*pFirst-32==*pLast||*pFirst==*pLast)) {pFirst++,pLast++;} if(*pFirst==*pLast) { dwKernelBase=(DWORD)pPLD->DllBase; break; } pPLD=(LDR_DATA_TABLE_ENTRY*)pPLD->InLoadOrderModuleList.Flink; }
2、kernerl32.dll 基址找到后就好办了:根据PE的导出表能找到GetProcAddr的地址,核心代码如下:
MAGE_DOS_HEADER *pIDH=(IMAGE_DOS_HEADER *)dwKernelBase; IMAGE_NT_HEADERS *pINGS=(IMAGE_NT_HEADERS *)((DWORD)dwKernelBase+pIDH->e_lfanew); IMAGE_EXPORT_DIRECTORY *pIED=(IMAGE_EXPORT_DIRECTORY*)((DWORD)dwKernelBase+pINGS ->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress); DWORD *pAddOfFun_Raw=(DWORD*)((DWORD)dwKernelBase+pIED->AddressOfFunctions); WORD *pAddOfOrd_Raw=(WORD*)((DWORD)dwKernelBase+pIED->AddressOfNameOrdinals); DWORD *pAddOfNames_Raw=(DWORD*)((DWORD)dwKernelBase+pIED->AddressOfNames); DWORD dwCnt=0; char *pFinded=NULL,*pSrc=szGetProcAddr; for(;dwCnt<pIED->NumberOfNames;dwCnt++) { pFinded=(char *)((DWORD)dwKernelBase+pAddOfNames_Raw[dwCnt]); while(*pFinded &&*pFinded==*pSrc) {pFinded++;pSrc++;} if(*pFinded == *pSrc) { pGetProcAddress=(PGETPROCADDRESS)((DWORD)dwKernelBase+pAddOfFun_Raw[pAddOfOrd_Raw[dwCnt]]); break; } pSrc=szGetProcAddr; }
有了GetProcAddr,又能继续查找LoadLibrary(也在kernerl32.dll里面)的地址,如下:
pLoadLibrary=(PLOADLIBRARY)pGetProcAddress((HMODULE)dwKernelBase,szLoadLibrary);
这下有了LoadLibrary和GetProcAddr两大函数地址,任何dll的任何函数入口都能找到了,比如MessageBox,如下:
pMessageBox=(PMESSAGEBOX)pGetProcAddress(pLoadLibrary(szUser32),szMessageBox); char szTitle[]={\'S\',\'h\',\'e\',\'l\',\'l\',\'C\',\'o\',\'d\',\'e\',0}; char szContent[]={0x48,0x65,0x6c,0x6c,0x6f,0x20,0x57,0x6f,0x72,0x6c,0x64,0x20,0x21,0}; pMessageBox(NULL,szContent,szTitle,0);
完整代码如下:(所有代码都写入main,方便下一步提取)
#include<windows.h> int main() { typedef DWORD (WINAPI *PGETPROCADDRESS) (HMODULE hModule,LPCSTR lpProcName); typedef int (WINAPI * PMESSAGEBOX) (HWND hWnd,LPCTSTR lpText,LPCTSTR lpCaption,UINT uType); typedef HMODULE (WINAPI * PLOADLIBRARY) (LPCTSTR lpFileName); typedef struct UNICODE_STRING { USHORT Length; USHORT MaximumLength; PWSTR Buffer; }UNICODE_STRING; typedef struct PEB_LDR_DATA{ DWORD Length; BYTE initialized; PVOID SsHandle; LIST_ENTRY InLoadOrderModuleList; LIST_ENTRY InMemoryOrderModuleList; LIST_ENTRY InInitializationOrderModuleList; VOID * EntryInProgress; }PEB_LDR_DATA; typedef struct LDR_DATA_TABLE_ENTRY { LIST_ENTRY InLoadOrderModuleList; LIST_ENTRY InMemoryOrderModuleList; LIST_ENTRY InInitializationOrderModuleList; void* DllBase; void* EntryPoint; ULONG SizeOfImage; UNICODE_STRING FullDllName; UNICODE_STRING BaseDllName; ULONG Flags; SHORT LoadCount; SHORT TlsIndex; HANDLE SectionHandle; ULONG CheckSum; ULONG TimeDateStamp; }LDR_DATA_TABLE_ENTRY; LDR_DATA_TABLE_ENTRY *pPLD=NULL,*pBeg=NULL; PGETPROCADDRESS pGetProcAddress=NULL; PMESSAGEBOX pMessageBox=NULL; PLOADLIBRARY pLoadLibrary=NULL; WORD *pFirst =NULL,*pLast=NULL; DWORD ret =0,i=0; DWORD dwKernelBase=0; char szKernel32[]={\'k\',0,\'e\',0,\'r\',0,\'n\',0,\'e\',0,\'l\',0,\'3\',0,\'2\',0,\'.\',0,\'d\',0,\'l\',0,\'l\',0,0,0}; char szUser32[]={\'U\',\'S\',\'E\',\'R\',\'3\',\'2\',\'.\',\'d\',\'l\',\'l\',0}; char szGetProcAddr[]={\'G\',\'e\',\'t\',\'P\',\'r\',\'o\',\'c\',\'A\',\'d\',\'d\',\'r\',\'e\',\'s\',\'s\',0}; char szLoadLibrary[]={\'L\',\'o\',\'a\',\'d\',\'L\',\'i\',\'b\',\'r\',\'a\',\'r\',\'y\',\'A\',0}; char szMessageBox[]={\'M\',\'e\',\'s\',\'s\',\'a\',\'g\',\'e\',\'B\',\'o\',\'x\',\'A\',0}; __asm{ mov eax,fs:[0x30] mov eax,[eax+0x0c] add eax,0x0c mov pBeg,eax mov eax,[eax] mov pPLD,eax } // 遍历找到kernel32.dll while(pPLD!=pBeg) { pLast=(WORD*)pPLD->BaseDllName.Buffer; pFirst=(WORD*)szKernel32; while(*pFirst && (*pFirst-32==*pLast||*pFirst==*pLast)) { pFirst++,pLast++;} if(*pFirst==*pLast) { dwKernelBase=(DWORD)pPLD->DllBase; break; } pPLD=(LDR_DATA_TABLE_ENTRY*)pPLD->InLoadOrderModuleList.Flink; } // 遍历kernel32.dll的导出表,找到GetProcAddr函数地址 IMAGE_DOS_HEADER *pIDH=(IMAGE_DOS_HEADER *)dwKernelBase; IMAGE_NT_HEADERS *pINGS=(IMAGE_NT_HEADERS *)((DWORD)dwKernelBase+pIDH->e_lfanew); IMAGE_EXPORT_DIRECTORY *pIED=(IMAGE_EXPORT_DIRECTORY*)((DWORD)dwKernelBase+ pINGS->OptionalHeader. DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress); DWORD *pAddOfFun_Raw=(DWORD*)((DWORD)dwKernelBase+pIED->AddressOfFunctions); WORD *pAddOfOrd_Raw=(WORD*)((DWORD)dwKernelBase+pIED->AddressOfNameOrdinals); DWORD *pAddOfNames_Raw=(DWORD*)((DWORD)dwKernelBase+pIED->AddressOfNames); DWORD dwCnt=0; char *pFinded=NULL,*pSrc=szGetProcAddr; for(;dwCnt<pIED->NumberOfNames;dwCnt++) { pFinded=(char *)((DWORD)dwKernelBase+pAddOfNames_Raw[dwCnt]); while(*pFinded &&*pFinded==*pSrc) {pFinded++;pSrc++;} if(*pFinded == *pSrc) { pGetProcAddress=(PGETPROCADDRESS)((DWORD)dwKernelBase+pAddOfFun_Raw[pAddOfOrd_Raw[dwCnt]]); break; } pSrc=szGetProcAddr; } // 有了GetProcAddr 可以获得任何api pLoadLibrary=(PLOADLIBRARY)pGetProcAddress((HMODULE)dwKernelBase,szLoadLibrary); pMessageBox=(PMESSAGEBOX)pGetProcAddress(pLoadLibrary(szUser32),szMessageBox); // 使用函数 char szTitle[]={\'S\',\'h\',\'e\',\'l\',\'l\',\'C\',\'o\',\'d\',\'e\',0}; char szContent[]={0x48,0x65,0x6c,0x6c,0x6f,0x20,0x57,0x6f,0x72,0x6c,0x64,0x20,0x21,0}; pMessageBox(NULL,szContent,szTitle,0); return 0; }
2、shellcode提取:从IDA看,main函数从401000开始,
在4012E0结束,总长度2e0;
这里可以直接在hexview查看二进制编码,可以直接从401000复制到4012E0,这段代码插入其他exe代码的入口点
3、随变找个exe,比如下面这个变量位置测试的exe,查看发现其入口点是4796:
12bc入口+2e0(shellcode长度)=159c,先删除这部分代码:
再把shellcode复制过来:
再次运行exe:能看到弹窗,达到注入代码的目的:
-----------------------------------------------------------分割线------------------------------------------------------------------------------------------------------------------------------
内存和数据相关区域的分工:
(1)栈(stack):由编译器进行管理,自动分配和释放,存放函数调用过程中的各种参数、局部变量、返回值以及函数返回地址;
(2)堆(heap):用于程序动态申请分配和释放空间。C语言中的malloc和free,C++中的new和delete均是在堆中进行的,还有windows驱动编程常用的ExAllocatePool;正常情况下,程序员申请的空间在使用结束后应该释放,若程序员没有释放空间,则程序结束时系统自动回收。堆内存的好处:只要程序员不主动释放,且程序运行不结束,这块数据会一直存在;这个特性可以用来隐藏驱动(https://www.cnblogs.com/theseventhson/p/13170445.html);
(3)全局(静态)存储区:分为DATA段和BSS段。DATA段(全局初始化区)存放初始化的全局变量和静态变量;BSS段(全局未初始化区)存放未初始化的全局变量和静态变量。程序运行结束时自动释放。其中BBS段在程序执行之前会被系统自动清0,所以未初始化的全局变量和静态变量在程序执行之前已经为0;
(4)文字常量区:存放常量字符串,程序结束后由系统释放;
其中,栈内存存放的数据仅仅在函数调用过程使用,结束后就没用了,所以编译器会增加分配(esp-xxx)和释放(esp+xxx)的代码; 但全局变量和静态变量要求在任何函数都能使用,所以不能存放在栈,只能放在DATA和BSS段,等程序运行结束后由操作系统回收;代码实验如下:
第一次的代码(就是上面用来做注入测试的storPosition.exe):
#include <stdio.h> #include <stdlib.h> int k1 = 1; int k2; static int k3 = 2; static int k4; int main( ) { static int m1=2, m2; int i=1; char*p; char str[10] = "hello"; char* q = "hello"; p= (char *)malloc( 100 ); free(p); printf("栈区-变量地址 i:%p\\n", &i); printf(" p:%p\\n", &p); printf(" str:%p\\n", str); printf(" q:%p\\n", &q); printf("堆区地址-动态申请:%p\\n", p); printf("全局外部有初值 k1:%p\\n", &k1); printf(" 外部无初值 k2:%p\\n", &k2); printf("静态外部有初值 k3:%p\\n", &k3); printf(" 外静无初值 k4:%p\\n", &k4); printf(" 内静态有初值 m1:%p\\n", &m1); printf(" 内静态无初值 m2:%p\\n", &m2); printf("文字常量地址 :%p, %s\\n",q, q); printf("程序区地址 :%p\\n",&main); return 0; }
各种变量地址的分布:
第二次的代码:和第一次比增加了3个局部变量:m、j和q1,并打乱了顺序:
#include <stdio.h> #include <stdlib.h> int k1 = 1; int k2; static int k3 = 2; static int k4; int main( ) { char*p; int m=3; static int m1=2, m2; int j=2,i=1; char str[10] = "hello"; char* q = "hello"; char q1[] = {\'h\',\'e\',\'l\',\'l\',\'o\',\'\\0\'}; p= (char *)malloc( 100 ); free(p); printf("栈区-变量地址 i:%p\\n", &i); printf("栈区-变量地址 j:%p\\n", &j); printf("栈区-变量地址 m:%p\\n", &m); printf(" p:%p\\n", &p); printf(" str:%p\\n", str); printf(" q:%p\\n", &q); printf(" q1=%s:%p\\n", q1, &q1); printf("堆区地址-动态申请:%p\\n", p); printf("全局外部有初值 k1:%p\\n", &k1); printf(" 外部无初值 k2:%p\\n", &k2); printf("静态外部有初值 k3:%p\\n", &k3); printf(" 外静无初值 k4:%p\\n", &k4); printf(" 内静态有初值 m1:%p\\n", &m1); printf(" 内静态无初值 m2:%p\\n", &m2); printf("文字常量地址 :%p, %s\\n",q, q); printf("程序区地址 :%p\\n",&main); return 0; }
各种变量地址的分布:
通过对比可以发现:静态变量、全局变量只要有初值,地址都是固定的;下面是更详细的说明:同样都是字符串,s1存放在栈,函数执行完返回后该区域被收回;s2存放在字符常量,程序结束后才会被释放;
最后:参考别人的文章如下:
https://b0ldfrev.gitbook.io/note/windows_operating_system/windows-xia-tong-yong-shellcode-yuan-li
https://www.bilibili.com/video/BV1y4411k7ch?p=6
https://blog.csdn.net/yangquanhui1991/article/details/51786380
以上是关于windows:shellcode 原理的主要内容,如果未能解决你的问题,请参考以下文章
windows:shellcode 代码远程APC注入和加载