#> RESTKHZ _

休止千鹤 | 我依旧是一名平凡的学生

Shellcode初探[3x]: 自制实用shellcode加载器,绕过杀软(静态)

  休止千鹤  |    14/03/2025

上次写这个系列的文章已经过于很长时间了,整个系列也已经歪到研究静态绕过去了。
本文是对第三篇的补充。

我们之前介绍过:

在第三篇中,我们用了简单的思路:

  • 对shellcode每个字节+1-1
  • 对敏感函数名称进行+1-1混淆
  • 动态函数加载

就这样,隐藏了shellcode和IAT中的敏感函数,从而做到绕过部分杀软。
如果你对基础还不熟悉,可以看看之前的文章。

其实免杀还有一个思路:通常,由于自己制作的加载器小规模使用很难受到关注,所以自制工具。自己做的自己用。

2025年,那么之前提到的静态扫描规避思路真的还有任何实用价值吗?
当然啦,说回来,+1-1搞个exe还是太弱了。
基于上一篇文章的思路,我做了另一个工具。已经做成一个处理脚本放在GitHub上了:

GitHub: ShellcodeEncrypt2DLL

VT

当然了,如果你想让工具活久一点,自制工具最好还是没事别放VT上了。这个我自己打算分享出来放Github上。被人丢VT上面是迟早的事。

那有小伙伴要问了:“啊,不放VT,还能放哪个上面测试呢?”
免费的antiscan.me已经没了。

用Kleenscan。他们有比较新的杀毒软件引擎。大概40个,他们不会把样本提交。
使用方法类似VT。有免费额度。

我放一个(有他们网站生成的推广链接)
Kleenscan

谈谈ShellcodeEncrypt2DLL

和之前那篇文章思路类似,我们对shellcode和敏感系统函数名称字符串进行了处理。只不过之前是+1-1,我们这次直接用AES进行加密,编译成DLL。(别看到DLL就没兴趣关网页了,下面我会解释为什么用DLL)

最初设计的时候考虑到为了一定程度上的防止被分析,所以我稍稍做了点”设计“。
生成的DLL分成了两种模式:

  • 独立模式:这种模式密钥被硬编码进去了。所以会更容易被逆向分析。但是在做劫持,搞PrintNightmare类似场景,不方便传参的时候的时候可以用。
  • 非独立模式:如果你有机会可以传入参数,更推荐用这个模式。这样密码和DLL分离,如果仅仅只是DLL被捕获,shellcode也很难被还原出来。

简而言之,这个脚本可以自动编译生成一个DLL. 其中的shellcode和需要用到的一些函数都会被AES加密。可以选择DLL中是否包含KEY。如果选择不包含key的话,那你需要在使用时想办法自己传入KEY。

当然这个东西主要还是在针对静态查杀的绕过。没有考虑内存扫描什么的。也就是说在运行起来以后内存里面还是有一个shellcode躺在内存里的…

为什么是DLL?

  • 因为exe被针对太狠了。同样的恶意代码,做成exe在VT上一般情况下都会比DLL被杀得更狠。
  • DLL具有很强的灵活性。它可以直接用rundll32运行以外,也可以用一个exe拉起来做sideload。同样,我们还可以用PowerShell,python等等方法加载DLL。
  • 我们还可以用hijack的方式劫持正常应用的DLL。
  • 还有像是PrintNightmare这种漏洞利用也要上传恶意DLL,exe没用。

为什么是AES

+1-1所做的仅仅只是混淆,不那么直接的能被杀毒软件找到字符串识别。我们之前隐藏了shellcode和函数名称,很好。但是一旦被逆向,很轻松的就会发现这里只是对字节做+1-1,于是shellcode被逆向,文件标记为恶意,C2地址暴露。
用AES可以防止shellcode被逆向出来。当然,前提是不要把KEY硬编码进去。

为什么不是XOR呢?

我曾分析过一些XOR玩法,感觉更多的还是在做混淆而非加密。
常见的,用一个字节的密钥对着shellcode一个字节一个字节xor过去。密钥就一个字节也就256种可能,就不说你有没有密钥硬编码了,这暴力跑一遍其实也不费劲。
至于xor密钥重复使用,可能还是已知明文。
比如我看你这里加密字符串长度可能是VirtualAlloc,我直接明文和你密文xor一下就有可能得到正确密钥。
如果我猜对了,我会得到长度和字符串一致,每个字节都相同的输出,这个字节就是密钥。

如图,密钥k=69='E',明文p='VirtualAlloc',密文是c

xor_problem

那么你不打算复用,你用了一套PRG,套种子做密钥生成和shellcode,敏感函数长度一致的密钥,那也很麻烦…
另外,不负责任地说,XOR在循环里单字节处理一块内存似乎有时候也被当作特征。

虽然事后证明,其实AES也没那么好。系统AES的API调用在IAT中也被怀疑。
怎么办呢?
动态加载,函数名+1-1 XD

尝试达成VT:0/73

当时做出来,还是有3家杀毒软件报毒的。后面我有试图去绕过剩下的杀毒软件,
这三个:Cynet, Kaspersky, Rising。

怎么确定哪里敏感?
我用的方法是:在PE的段里填充成0. 比如原来的.text,我直接整个section全部填充覆盖成0x00.

为了方便阅读,提前给各位复习一下:

  • .idata段里面有导入表(IAT),会有DLL导入函数的字符串。比如你直接调用AES函数,那么函数名称的字符串在这里。我们可以用动态加载解决。这样函数名称不会出现在这里。
  • 而程序运行时的字符串常量都被放在.rdata段里面,所以shellcode和一些其它常量字符串也会放在这里。也就是说,你在编写动态加载的时候,如果你的敏感函数名的字符串直接被定义,就会保存在这里。

测试:

  • 我试着填充.text和.data,这三家一样报毒,给出的理由和没填充都一样。
  • 又试着单独填充了.rdata,这次Kaspersky没报毒了。
  • 仅填充.idata,这次换rising没报毒。

后面填充过edata,没影响。
总之,kaspersky可能就是觉得.rdata里那些加密的数据可疑。而rising可能觉得.idata导入表是特征。

cynet我就不清楚了。
于是我把idata,rdata都填充掉。
这次这三个都没报毒。但是DeepInstinct报毒了…

绕过Kaspersky

我发现似乎每次都是Kaspersky先报毒,然后过一段时间Rising对相同样本才报。而且分类和Kaspersky大差不差。但是他们标记的特征是完全不同的。

当然Kaspersky是最坑的一个。我最初以为是.rdata的熵太高了。毕竟加密后的shellcode在里面。

shellcode_entropy

如图,这里是shellcode,经过AES加密以后,数据几乎是随机的。熵7.5,逼近8。基本从熵就能看出来这里有加密数据。我想的是,这里搭配上解密函数调用,就很可疑。

熵逼近8, 意味着接近于一个字节的最大信息表达能力。一个字节有8bit,可以表达256种可能。这里的熵是这样来的$\log_2(256)=8$。 两个不同的字节,只有两种可能,1bit就能表示。所以两个不同的字节在这里计算出的熵是1.类似的,4个不同字节可以用2bit表达,熵是2. 而这里7.5几乎cover了256种可能了。正常的话(个人经验),英文字符串熵应该在4-5之间。所以我们可以在里面加入一样的字节来降低熵。大概需要shellcode两倍才能压到4.5左右。

于是我在shellcode里面每个字节之间添加一个字节的00。并且在结尾放上一段和shellcode等长的FF填充来进一步降低熵。

这样一折腾。恩…结果,没用。但是我没有移除这个功能。缺点就是:shellcode体积会变成原来的三倍。

所以到底是怎么回事?
Mingw-w64在编译后往这里塞了一个错误信息。里面有字符串VirtualProtect。蛋疼的是这玩意儿我是真没找到怎么在编译的时候去掉。
所以我做了一个patch.py。只能在编译好以后用python在.rdata里替换掉这个字符串了。

signiture

Rising绕过

根据填充.idata后,Rising就安静了,说明特征可能在.idata里。简单测了一下,AES的API调用可能是问题根源。

那就,动态加载AES函数并且做了+1-1后让字符串认不出来后Rising绕过。

神奇的是Cynet同时也绕过了。Cynet貌似是依靠综合评分的。

总结

还是那句话,其实对于静态扫描绕过而言,挺简单的。很多时候,字符串,函数名,藏好就行。
至于沙箱,内存扫描,以后再说。

后话

过了一个星期,回头在看,这个工具也被针对了。已经被
Avast和AVG标记,最近又多了一个Trellix。
有些杀软迷和网管应该知道以后应该选择哪些杀毒软件了吧…


Views:

 Comments


Furkan:

Isn't it possible to run the DLL on its own? Let's say I don't want to use it as "rundll32 <path_to_dll>,EPoint". Let's say I want to use it only as "rundll32 loader.dll". What can we do about this?

 Reply


Furkan:

Can I use this as a standalone DLL just for the printnightmare vulnerability? That's what I was actually asking.

 Reply


restkhz:(admin)

Furkan said : Can I use this as a standalone DLL just for the printnightmare vulnerability? That's what I was actually asking.
Sure. Use the standalone mode. I've tested this in the lab with Printnightmare vuln, it works fine. The reason? Standalone mode DLL can run itself automatically when it's attached to a process. You can see this in `template.cpp` , in DllMain function. And, you got hundred ways to execute a function in DLL. Don't just let a rundll32 limit you.

 Reply


Furkan:

I will use standalone mode. Thank you very much for the information. Would it be better to run the "patch.py" script?

 Reply