每当你听到ShellCode一定会想到病毒与安全。其实ShellCode并没有你想想中的那么难,它有一个特点就是把它嵌入到任何进程中都能够运行。是不是感觉很牛逼。但是我们分析一下什么代码能够不依赖任何环境呢?首先这段代码不能够有常量区、静态区数据。也就是说不能够有全局变量。还有不能有类似char str[]={"hello word"};这样的数据,因为这样的数据在常量区。同时不能够有系统调用和函数调用。当你代码包含以上这些条件。那么恭喜你已经完成了一个ShellCode。下面我们就简单分析一个windows下弹Messagebox这段代码如何书写。
首先需要获取到KERNEL32.DLL的基地址。我们可以用以下两种方法,其原理都是一样的,我希望你和我一样想知道为什么这么写,那么我们来探究为什么这样写。
_asm
{
MOV EAX, DWORD PTR FS : [0x30]//; 获取PEB基址
MOV EAX, DWORD PTR DS : [EAX + 0xC]//; 获取PEB_LDR_DATA结构指针
MOV ESI, DWORD PTR DS : [EAX + 0x1C]//; 获取InInitializationOrderModuleList成员指针
LODS DWORD PTR DS : [ESI]//; 把ESI地址里的值给EAX,同时ESI自己加4,相当于获取下一个节点
MOV EBX, DWORD PTR DS : [EAX + 8]//; 取其基地址,该结构当前包含的是kernel32.dll
MOV dwKernelBase, EBX
}
_asm
{
mov eax, DWORD PTR FS:[0x30]//+0x030 ProcessEnvironmentBlock : Ptr32 _PEB*
mov eax, DWORD PTR DS:[eax + 0x0c]// +0x00c Ldr : Ptr32 _PEB_LDR_DATA *
mov eax, DWORD PTR DS:[eax + 0x1c]// +0x01c InInitializationOrderModuleList : _LIST_ENTRY
mov pBEG, eax //pBEG自己定义的PVOID
mov eax, [eax]//地址里的值指向下一个
mov pPLD, eax //pPLD自己定义的PVOID
}
//遍历找到kernel32.dll
do
{
PVOID BaseAddress = (PVOID)*((PDWORD)((DWORD)pPLD + 0x08));
PVOID FullDllName = (PVOID)*((PDWORD)((DWORD)pPLD + 0x20));
WCHAR* szname = (WCHAR*)FullDllName;
pLast = (WORD *)FullDllName;
pFirst = (WORD *)szKernel32;
while (*pFirst && *pFirst == *pLast)
{
pFirst++;
pLast++;
}
if (*pFirst == *pLast)
{
dwKernelBase = (DWORD)BaseAddress;
break;
}
pPLD = (PVOID)*((PDWORD)pPLD);
} while (pPLD != pBEG);
要想理解上面的代码就要知道FS:[0] 相当于基地址为当前线程的线程环境块(TEB),所以该段也被称为TEB段。下面就是TEB的结构体
/*
cefclient!_TEB
+0x000 NtTib : _NT_TIB
+0x01c EnvironmentPointer : Ptr32 Void
+0x020 ClientId : _CLIENT_ID
+0x028 ActiveRpcHandle : Ptr32 Void
+0x02c ThreadLocalStoragePointer : Ptr32 Void
+0x030 ProcessEnvironmentBlock : Ptr32 _PEB//进程环境块 PEB
+0x034 LastErrorValue : Uint4B
+0x038 CountOfOwnedCriticalSections : Uint4B
+0x03c CsrClientThread : Ptr32 Void
+0x040 Win32ThreadInfo : Ptr32 Void
*/
我们看到了PEB在偏移0x30的位置所以你很好理解 mov eax, DWORD PTR FS:[0x30]这句汇编了吧。
/**/
这下我们理解了mov eax, DWORD PTR DS:[eax + 0x0c]// +0x00c Ldr : Ptr32 _PEB_LDR_DATA *
/*对应mov eax, DWORD PTR DS:[eax + 0x1c]// +0x01c InInitializationOrderModuleList : _LIST_ENTRY
下面我们来看看
mov pBEG, eax //首先用pBEG保存第一个链表的地址
mov eax, [eax]//地址里的值指向下一个
mov pPLD, eax//pPLD保存下一个指向的地址
这是一张我在网上找到的图 感觉很形象理解上面的结构体
但是我们是在第三个list做的选着下一个节点。所以这个图有一些问题,但是原理是一样的。是不是知道我们如何找到KERNEL32.DLL的基地址了。我推荐使用第二种方法查找基地址。第一中在win7以上系统才可以。第二种通过对比字符串确定基地址更为准确一些。
我们已经找到了KERNEL32.DLL的基地址了,下面如何找到GetProcAddress函数地址了。这需要你对PE文件有一些了解。知道导出表在那个位置。代码如下
IMAGE_DOS_HEADER *pIDH = (IMAGE_DOS_HEADER*)dwKernelBase;//获取基地址DOS头
IMAGE_NT_HEADERS *pINGS = (IMAGE_NT_HEADERS*)((DWORD)dwKernelBase + pIDH->e_lfanew);//找到NT头
IMAGE_EXPORT_DIRECTORY *pIED = (IMAGE_EXPORT_DIRECTORY*)((DWORD)dwKernelBase + pINGS->OptionalHeader.DataDirectory[0].VirtualAddress);//找到导出表位置
DWORD *pAddOffun_Raw = (DWORD*)((DWORD)dwKernelBase + pIED->AddressOfFunctions);//导出表对应的三个地址,他们之间多关系我就不讲了自己查看导出表
WORD *pAddOfOrd_Raw = (WORD*)((DWORD)dwKernelBase + pIED->AddressOfNameOrdinals);
DWORD *pAddOfofNames_Raw = (DWORD*)((DWORD)dwKernelBase + pIED->AddressOfNames);
下面我们要获取具体GetProcAddress函数地址
// PE(导出表)->找导出函数
for (;dwCntNumberOfNames;dwCnt++)
{
pFinded = (char*)((DWORD)dwKernelBase + pAddOfofNames_Raw[dwCnt]);//名称表对应多名称地址
while (*pFinded && *pFinded == *pSrc)//对比GetProcAddress字符串
{
pFinded++;
pSrc++;
}
if (*pFinded == *pSrc)//对比成功
{
pGetProcAddress = (PGETPROCADDRESS)((DWORD)dwKernelBase + pAddOffun_Raw[pAddOfOrd_Raw[dwCnt]]);//名称表对应序号表地址里的内容就是对应多函数地址
break;
}
pSrc = szGetProcAddr;
}
这样我们就找到了GetProcAddress函数地址,有了这个函数地址我们就可以获取任意我们想加载的函数了
//获取其他函数地址
pLoadLibrary = (PLOADLIBRARY)pGetProcAddress((HMODULE)dwKernelBase, szLoadLibrary);
pMessageBox = (PMESSAGEBOX)pGetProcAddress((HMODULE)pLoadLibrary((LPCTSTR)szUser32), szMessageBox);
char strtest[] = { 'S','h','e','l','l','C','o','d','e',0 };
char strContent[] = { 'l','i','u','g','x',0 };
pMessageBox(NULL, (LPCTSTR)strtest, (LPCTSTR)strContent, 0);
这样我们调用成功MessageBox函数了。当你把上面一段代码转成硬编码,之后就可以嵌入其他进程中运行了。