Windows ShellCode提取加载与免杀
这篇文章仅讲windows下的,linux下比较简单,之后可能会写
shellcode是一段用于利用软件漏洞而执行的代码
编写
首先说明,shellcode编写可以用c也可以直接用汇编来写,但难度不在一个层级,我们选择c》
vs我用的是vs2013,本来用的2008,但是找不到汇编窗口
下面是windows shellcode编写的步骤
获取kernel32.dll 基地址;
定位 GetProcAddress函数的地址;
使用GetProcAddress确定 LoadLibrary函数的地址;
然后使用 LoadLibrary加载DLL文件(例如user32.dll);
使用 GetProcAddress查找某个函数的地址(例如MessageBox);
指定函数参数;
调用函数。
首先要注意shellcode的地址无关
原则
char* arr = "test";
我们看到这么写的话 test存放在一个固定地址,而不同windows下的内存地址是不同的,所以我们不能将地址写死
char cmd[] = { 'c','a','l','c','\x00'};
但上面这种写法就不会有固定地址,但这样写需要用\x00
来截断
现在地址无关解决,下一步是函数调用,我们需要kernel32.dll 基地址,但是由于ASLR导致dll可以加载到不同的内存位置,需要动态定位
PEB结构位于固定内存位置,所以我们可以通过PEB来获取。
读取PEB结构
跳转到0xC偏移处读取Ldr指针
跳转到0x14偏移处读取 InMemoryOrderModuleList字段
如果你不太懂上面三步,尽量多思考一下下面的内容
进程:是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,可以简单的理解为,计算机中每运行的一个软件都是一个进程。
PEB:是一个位于所有进程内存中固定位置的结构体。此结构体包含关于进程的有用信息,如可执行文件加载到内存的位置,模块列表(DLL),指示进程是否被调试的标志,还有许多其他的信息。
typedef struct _PEB {
BYTE Reserved1[2];
BYTE BeingDebugged;
BYTE Reserved2[1];
PVOID Reserved3[2];
PPEB_LDR_DATA Ldr;
PRTL_USER_PROCESS_PARAMETERS ProcessParameters;
BYTE Reserved4[104];
PVOID Reserved5[52];
PPS_POST_PROCESS_INIT_ROUTINE PostProcessInitRoutine;
BYTE Reserved6[128];
PVOID Reserved7[1];
ULONG SessionId;
} PEB, *PPEB;
上面是微软关于PEB结构体的官方文档,
上面内容的一些概念
BYTE表示1个字节
PVOID表示1个指针(或1个内存地址,ps:一定要弄明白指针这东西,很重要)在0x86中一个地址占四个字节
PPEB_LDR_DATA是1个指针,指向自定义结构体PEB_LDR_DATAPEB_LDR_DATA
BeingDebugged标志是1个字节
Reserved1[2]是两个BYTE的数组,占两个字节
Reserved3[2]是两个PVOID指针的数组,占八个字节
我们重点关注下PEB_LDR_DATA(跳转到0xC偏移处读取Ldr指针 )
跳转偏移计算:2 + 1 + 1 + 8 = 12 = 0xC
typedef struct _PEB_LDR_DATA {
BYTE Reserved1[8];
PVOID Reserved2[3];
LIST_ENTRY InMemoryOrderModuleList;
} PEB_LDR_DATA, *PPEB_LDR_DATA;
关注LIST_ENTRY InMemoryOrderModuleList(跳转到0x14偏移处读取 InMemoryOrderModuleList字段)
跳转偏移计算:8 + 12 = 20 = 0x14
typedef struct _LIST_ENTRY {
struct _LIST_ENTRY *Flink;
struct _LIST_ENTRY *Blink;
} LIST_ENTRY, *PLIST_ENTRY;
LIST_ENTRY结构是一个简单的双向链表,包含指向下一个元素(Flink)的指针和指向上一个元素的指针(Blink)
InMemoryOrderModuleList字段是一个指针,指向LDR_DATA_TABLE_ENTRY 结构体上的LIST_ENTRY字段。但是它不是指向
LDR_DATA_TABLE_ENTRY 起始位置的指针,而是指向这个结构的InMemoryOrderLinks字段。
上面操作完到了内存首个模块
的InMemoryOrderLinks元素,这个模块
是一个可执行文件(.exe),我们需要去遍历加载到内存的dll文件。
具体通过InMemoryOrderModuleList.Flink来访问第二个已加载的模块,通过循环操作就可以遍历所有已加载的模块
calc.exe
ntdll.dll
kernel32.dll
当我们通过遍历得到kernel32.dll后就可以完成下面操作了
获取kernel32.dll 基地址;
定位 GetProcAddress函数的地址;
使用GetProcAddress确定 LoadLibrary函数的地址;
然后使用 LoadLibrary加载DLL文件(例如user32.dll);
使用 GetProcAddress查找某个函数的地址(例如MessageBox);
指定函数参数;
调用函数。
这边有个代码模版,如果你实在不会写可以参考这个模版来理解上述操作
#include<Windows.h>
#include<winnt.h>
#include<winternl.h>
DWORD getHash(char* str) {
DWORD h = 0;
while (*str) {
h = (h >> 13) | (h << (32 - 13));
h += *str >= 'a' ? *str - 32 : *str;
str++;
}
return h;
}
DWORD getunicodeHash(wchar_t* str) {
DWORD h = 0;
PWORD ptr = (PWORD)str;
while (*ptr) {
h = (h >> 13) | (h << (32 - 13));
h += (BYTE)(*ptr) >= 'a' ? (BYTE)(*ptr) - 32 : (BYTE)(*ptr);
ptr++;
}
return h;
}
PVOID getWinExec() {
char dllname[] = { 'K','E','R','N','E','L','3','2','.','D','L','L','\x00' };
char api[] = { 'W','i','n','E','x','e','c','\x00' };
_PEB* peb = NtCurrentTeb()->ProcessEnvironmentBlock;
LIST_ENTRY* first = peb->Ldr->InMemoryOrderModuleList.Flink;
LIST_ENTRY* ptr = first;
do {
LDR_DATA_TABLE_ENTRY* dte = (LDR_DATA_TABLE_ENTRY*)((BYTE*)ptr - 0x8);
BYTE* baseAddress = (BYTE*)dte->DllBase;
ptr = ptr->Flink;
if (!baseAddress)
continue;
PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)baseAddress;
PIMAGE_NT_HEADERS ntHeader = (PIMAGE_NT_HEADERS)(baseAddress + dosHeader->e_lfanew);
DWORD iedRVA = ntHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress;
if (!iedRVA)
continue;
PIMAGE_EXPORT_DIRECTORY ied = (PIMAGE_EXPORT_DIRECTORY)(baseAddress + iedRVA);
if (getunicodeHash(((decltype(dte->FullDllName)*)(DWORD*)&(dte->Reserved4))->Buffer) == getHash(dllname)) {
DWORD* nameRVAs = (DWORD*)(baseAddress + ied->AddressOfNames);
for (DWORD i = 0; i < ied->NumberOfNames; i++) {
char* funcName = (char*)(baseAddress + nameRVAs[i]);
if (getHash(funcName) == getHash(api)) {
WORD ordinal = ((WORD*)(baseAddress + ied->AddressOfNameOrdinals))[i];
DWORD functionRVA = ((DWORD*)(baseAddress + ied->AddressOfFunctions))[ordinal];
return baseAddress + functionRVA;
}
}
}
} while (ptr != first);
return NULL;
}
void func() {
char exec[] = { 'c','a','l','c','\x00'};
decltype(WinExec)* myWinExec = (decltype(WinExec)*)getWinExec();
myWinExec(exec, 0);
}
int main()
{
func();
return 0;
}
上面代码执行完之后会弹出windows计算器
这里不讲windows可利用shellcode的编写,cs,msf都已经提供了很好用的shellcode
提取
shellcode的提取
使用c++开发代码
更改VisualStudio编译配置
生成exe
在IDA下打开生成的exe,获得机器码
开发代码我们已经完成了,接下来是编译
配置编译选项,下面很多是默认的
release在调试工具栏
使大小最小化
项目 - (你项目名称的)属性 - c/c++ - 优化 - 最大优化(优选大小)
内联函数扩展
项目 - (你项目名称的)属性 - c/c++ - 优化 - 函数扩展(只适用于_inline(Ob1))
启用内部函数
项目 - (你项目名称的)属性 - c/c++ - 优化 - 启用函数选择(是)
禁用安全检查(/Gs-)
项目 - (你项目名称的)属性 - c/c++ - 代码生成 - 安全检查(禁用)
启用函数级链接
项目 - (你项目名称的)属性 - c/c++ - 代码生成 - 启用函数级链接(是)
增量链接
项目 - (你项目名称的)属性 - 链接器 - 常规 -启用增量链接 (否)
生成映射文件
项目 - (你项目名称的)属性 - 链接器 - 调试 - 生成映射文件 (是)
映射文件名随便写
启用COMDAT折叠
项目 - (你项目名称的)属性 - 链接器 - 优化 - 启用COMDAT折叠(是)
函数顺序
项目 - (你项目名称的)属性 - 链接器 - 优化 - 函数顺序 (function_order.txt)
关闭SDL检查
项目 - (你项目名称的)属性 - c/c++ - SDL检查改为否
生成exe文件,用ida打开