《简单的vshell绕过火绒内存查杀》

1.起因

好久没有做vshell的免杀了,一直在做的是cobalt strike的免杀,因为算是第一个接触的C2我个人也是比较有念旧情节的,就一直没有换。

但是最近在帮朋友做免杀的时候发现了一个事情,vshell能正常上线 但是执行命令的时候会被秒杀,本着在学校没有事情做的原理,就研究一下吧。

2.开始

首先火绒内存查杀 高级设置全部打开

image-20260529155330674

首先我们上线一个vshell,观察一下情况

image-20260529155002125

我们可以看到 shellcode的生成并不像cobaltstrike那样存在stager和stagerless的选项,只有固定的一个stager。

我们申请tcp的监听器的,shellcode进行上线

实际测试文件管理操作不会被动态查杀

image-20260529155517255

截屏操作不会被查杀

image-20260529155540747

屏幕操作不会被查杀

image-20260529155607039

当我们点击交互命令的时候,就会秒删

image-20260529155721906

点击非交互式的时候 则会发现执行一个命令过后就会被删除

3.简单探测

通过上面试验的规则我们应该大概了解了一些情况 执行命令的时候会被删除。那么到底是为什么呢??

我们再次研究一下

首先关闭火绒(看看马子运行原理),看看这两个交互式和非交互式具体做了什么

交互式

我们使用SystemInformer.exe 查看进程属性,发现当启动交互终端的时候创建了一个子进程 winpty-agent.exe

image-20260529160215969

可以发现放在了临时目录下

image-20260529160414320

image-20260529160440106

至于这个到底是什么??这个是一个开源的项目,网上也有很多介绍,我就不多赘述了

https://github.com/rprichard/winpty

image-20260529160715027

关闭终端则这个进程关闭

非交互式

我们使用非交互式执行命令的时候,则没有这个进程

image-20260529161003965

这就证明是这个上线的agent充当了执行的终端

查看内存属性

image-20260529161130961

我们执行查看内存属性会发现,rwx两片 rx一片 并且这里没有物理文件支撑,很大概率就是我们的马子了

第一片rwx属性是Mapped代表的是共享的,这是我们小马存放的地址,为什么呢?因为我的加载器就是申请的映射内存然后写入执行的。

第二片属性是private私有的,并且点开一看

image-20260529161423419

4d5a(MZ标志) 5045(PE标志),就是一个PE头特征,妥妥的PE结构啊 这里应该就是大马的位置了。

后面我拿ida-mcp配合claude逆向这个内存,发现确实是大马,并且里面一堆特征,比如有关oss的字符串,还有那个开源终端,全部能逆向出来。

这片内存是一个很敏感的内存,为什么呢?这就是杀软和edr的一个最基本的检测了,详细可以看鸭哥的文章

image-20260529162121346

4.确定思路

现在思路就很明确了 交互式是上传一个终端程序,非交互式是自己上线的agent来做命令执行。

再加上 vshell的一个不开源的 go语言编译的(很难逆向),所以我不能改他上传终端那个逻辑,而是选择改内存。

那到底怎么做内存免杀呢?cs那里用的是sleepmask,在鹏瑶大佬的udrl发展那个议题中介绍了一些c2的演进在这里面也介绍了一下手法。鸭哥的文章也有介绍一下手法

image-20260529162609784

image-20260529162756910

当然火绒不需要这么费力,本着轻量级的,让用户良好体验的,火绒没有hook机制,也没有堆栈检测机制。

(当然我也只是一个小白,哪里说错了,大佬们也多多包涵)。

我一开始的思路就是找到大内存那片地址,然后抹除PE头就可以了。

我是直接hook了一下 VirtualAlloc 看大内存rwx申请的地址是什么

0000028780000000

image-20260529164120494

然后我们可以将这个地址 存入到全局变量后面再将其抹除PE头,我们可以看到第二次调用virtualAlloc的时候就是申请的这个大内存,那么第一次呢??第一次是我写的hook_loader申请的

image-20260529164243401

随后我写的逻辑是在第二次调用申请内存的时候记录地址和大小放在全局变量中,第三次申请的时候 直接开始抹除第二次申请的PE头,写的有点直接,但是这只是初步试探的过程,后续确

image-20260529164455804

此时我们再运行可以看到成功抹除了

image-20260529164930164

但是运行命令一样会被查杀

image-20260529165211725

那么为什么呢???

5.查看安全日志确定突破口

第一个查杀

image-20260529165315353

第二个内存查杀

image-20260529165400457

第三个内存查杀

image-20260529165454684

发现了吗???他们的虚拟地址是一样的都是0x0000000067700000

我们再运行一下看看该内存

image-20260529165712108

也是很明显的一个PE结构的内存,但是内存属性为RW。研究一下这个吧

加上或这个条件

image-20260529165857723

再看一下0x0000000067700000

1
2
3
4
5
6
7
[*] VirtualAlloc EXE page: 0000000067700000  size: 8208384
[*] VirtualAlloc flAllocationType : 12288 flProtect: 4
virtualNumber is 262
[*] VirtualAlloc EXE page: 0000000067700000 size: 1024
[*] VirtualAlloc flAllocationType : 4096 flProtect: 4
virtualNumber is 263

以可读可写的权限先以0000000067700000为基质申请了一片大内存 然后再在这之中申请了一个小内存

再加上内存中这是一个PE程序,他应该是先申请了一个rw的程序 再将PE写入内存然后伸展修复,再修复各节的属性。

这也是很常规的一个操作了

于是我们再hook一下 virtualprotec api来查看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
virtualNumber is 241
lpAddress is 0000000067701000 dwsize is 3265024,flNewProtect is 32,lpflOldProtect is 563084636
protectNum is 3
lpAddress is 0000000067A1F000 dwsize is 262656,flNewProtect is 4,lpflOldProtect is 563084636
protectNum is 4
lpAddress is 0000000067A60000 dwsize is 4177920,flNewProtect is 2,lpflOldProtect is 563084636
protectNum is 5
lpAddress is 0000000067E5C000 dwsize is 2560,flNewProtect is 2,lpflOldProtect is 563084636
protectNum is 6
lpAddress is 0000000067E5D000 dwsize is 2048,flNewProtect is 2,lpflOldProtect is 563084636
protectNum is 7
lpAddress is 0000000067E5E000 dwsize is 399872,flNewProtect is 4,lpflOldProtect is 563084636
protectNum is 8
lpAddress is 0000000067EC0000 dwsize is 512,flNewProtect is 2,lpflOldProtect is 563084636
protectNum is 9
lpAddress is 0000000067EC1000 dwsize is 5120,flNewProtect is 4,lpflOldProtect is 563084636
protectNum is 10
lpAddress is 0000000067EC3000 dwsize is 512,flNewProtect is 4,lpflOldProtect is 563084636
protectNum is 11
lpAddress is 0000000067EC4000 dwsize is 512,flNewProtect is 4,lpflOldProtect is 563084636
protectNum is 12
# 地址 大小 新属性 解读
3 0x67701000 ~3.1 MB 0x20 (EXECUTE_READ) 代码段:可执行+只读,典型的 .text 段属性
4 0x67A1F000 ~256 KB 0x04 (READWRITE) 数据段:可读写,可能是 .data 或堆
5 0x67A60000 ~4.0 MB 0x02 (READONLY) 只读数据:可能是 .rdata 或资源段
6 0x67E5C000 2.5 KB 0x02 (READONLY) 小块只读数据
7 0x67E5D000 2.0 KB 0x02 (READONLY) 同上,可能是导入表/重定位表的一部分
8 0x67E5E000 ~390 KB 0x04 (READWRITE) 可读写数据段
9 0x67EC0000 512 B 0x02 (READONLY) 极小块,可能是某个表/结构的头部
10 0x67EC1000 5 KB 0x04 (READWRITE) 小块可读写数据
11 0x67EC3000 512 B 0x04 (READWRITE) 同上
12 0x67EC4000 512 B 0x04 (READWRITE) 同上

这能证明 这是PE 文件内存映射后的节区属性修复。

image-20260529172050173

我们尝试抹除这片区域的PE头,因为这个地址申请的是固定的0000000067700000,这样就更简单了。

我直接通过CreateTimerQueueTimer创建一个时间回调执行,过5秒后抹除 该地址PE头

image-20260529172234121

image-20260529172205525

运行程序看见Hack The Planet 这个标志就代码已经成功抹除

image-20260529172329638

image-20260529172413843

此时再执行命令就不会被查杀

image-20260529172454763

这就是很简单的手法达成的内存查杀,当然实战中我们还是最后利用veh+sleep做成sleepmask这种吧,毕竟vshell好像没有睡眠机制,为什么呢???因为我hook了sleep发现并没有运行sleep

与此同时交互式的终端运行命令也修复了,交互式运行命令也不会被查杀

image-20260529172802310

9

6.最终演示视频

相比较原来的免杀的加载器 只添加一个回调函数

image-20260529174853939

一个时间序列5秒后调用,就能简单绕过内存查杀

image-20260529174937942

7.番外

我问了一下我朋友,不仅仅vshell的这片地址申请固定,cs的stager有一片地址申请的也是固定的

image-20260529174636928

当然后续vshell的内存免杀,就可以仿造着sleepmask那样 通过veh异常处理+CreateTimerQueue创建时间序列实现 内存属性浮动和休眠加密 执行解密。毕竟这样直接抹除PE头的手法还是太暴力了。哈哈


《简单的vshell绕过火绒内存查杀》
http://example.com/2026/05/30/《简单的vshell绕过火绒内存查杀》/
Author
John Doe
Posted on
May 30, 2026
Licensed under