Repository: ayoubfaouzi/al-khaser (language: cpp)

Issue:
[Improvement] ScanForModules does not support managed modules
title


Discussion / hints:
Can you be more specific about what isn't supported? Modules loaded into the process should be detected by the existing checks, regardless of whether they're .NET assemblies or native DLLs. Is there some specific .NET module enumeration mechanism that's missing?
> Can you be more specific about what isn't supported? Modules loaded into the process should be detected by the existing checks, regardless of whether they're .NET assemblies or native DLLs. Is there some specific .NET module enumeration mechanism that's missing?

Yes while debugging i figured it was not enumerating .NET modules.
Which of the module checks isn't detecting .NET modules? How are you injecting .NET modules into the process? From what I can see on my end .NET DLLs are being detected just fine.
My bad i got confused i thought it was enumerating the whole system process modules, So i guess it should be an idea for a new component of AntiAnalysis.

Alright i done further tests and made it enumerate all processes the thing is that the .NET modules were not listed.
My plan was to blacklist most of programs which are based on popular library's such as dnlib or asmresolver and/or if there is other debuggers that rely on an external known module.
@0x11DFE  What you want to enumerate are not PE Modules but .NET assemblies, which is different right ?

This tool is using native code, therefore you can't expect a .net binary attaching/hijacking it. Assuming it was managed code, then yeah you can list or enumerate assemblies loaded / referenced (like `Assembly.GetReferencedAssemblies()` ) then you can check if the assembly name is in the blacklist.

In addition to that, so if this tool enumerate the list of running processes, and eventually injects a managed dll to a managed process to find out if the remote process has one of the blacklisted assemblies, what would that entails ? it is still not our own process. (it is not like the other anti vm/debug tricks where you can assume if one debugger process is running in the system, then it is a  researcher/malware analyst machine.)
I'm still confused.

.NET assemblies are regular PE DLLs, and they appear in the results of `EnumProcessModules` when loaded into a process. They should also appear in the results of `NtQuerySystemInformation` when using `SystemModuleInformation`. It doesn't matter whether the host process is managed or native - they're loaded just like any other PE; they just happen to have a .NET metadata table in the PE header that tells the OS that the module needs the CLR. If you load a .NET DLL from disk into the process, it should appear in the process' module list in exactly the same way as any other DLL in a process is.

Are you referring to in-memory assemblies, such as ones loaded by `AppDomain.Load(byte[])`? That would certainly be something we should try to find in the Al-Khazer process memory, although I'm not sure of the best way. Process Hacker appears to use ETW to find .NET assemblies in a process. We'd need to check whether this is supported on non-.NET processes that are injected with dynamic assemblies.
> I'm still confused.
> 
> .NET assemblies are regular PE DLLs, and they appear in the results of `EnumProcessModules` when loaded into a process. They should also appear in the results of `NtQuerySystemInformation` when using `SystemModuleInformation`. It doesn't matter whether the host process is managed or native - they're loaded just like any other PE; they just happen to have a .NET metadata table in the PE header that tells the OS that the module needs the CLR. If you load a .NET DLL from disk into the process, it should appear in the process' module list in exactly the same way as any other DLL in a process is.
> 
> Are you referring to in-memory assemblies, such as ones loaded by `AppDomain.Load(byte[])`? That would certainly be something we should try to find in the Al-Khazer process memory, although I'm not sure of the best way. Process Hacker appears to use ETW to find .NET assemblies in a process. We'd need to check whether this is supported on non-.NET processes that are injected with dynamic assemblies.

Yes exactly they might be loaded from memory since i can only seem to find them using Process Hacker.
> @0x11DFE What you want to enumerate are not PE Modules but .NET assemblies, which is different right ?
> 
> This tool is using native code, therefore you can't expect a .net binary attaching/hijacking it. Assuming it was managed code, then yeah you can list or enumerate assemblies loaded / referenced (like `Assembly.GetReferencedAssemblies()` ) then you can check if the assembly name is in the blacklist.
> 
> In addition to that, so if this tool enumerate the list of running processes, and eventually injects a managed dll to a managed process to find out if the remote process has one of the blacklisted assemblies, what would that entails ? it is still not our own process. (it is not like the other anti vm/debug tricks where you can assume if one debugger process is running in the system, then it is a researcher/malware analyst machine.)

It was just an idea yeah it might have some false positives. But i do think its a better way rather than enumerating all process names and checking if it matches a given blacklist.
I've just had a look at some injected processes and I think we can detect .NET binaries being loaded into the process without needing to mess around with ETW.

When the CLR is attached to a process, regardless of whether or not it started out as a .NET process, it creates in-memory structures that describe the loaded managed modules. The structures look like this:

```
0000000000000000 0000000000000000 0000000000000000 0000000000000000
0000000000000000 0000000000000000 PPPPPPPPPPPPPPPP 00000000LLLLLLLL
0000000000000001 0000000000000002 0000000000000000 0000000000000002
0000000000000000 0000000000000002 0000000000000000
```

Where `PPPPPPPPPPPPPPPP` is a pointer to a UTF-16 string that names the module, and `LLLLLLLL` is the length of that string in bytes.

This data is always present inside a private committed memory region ~~that is 0x20000 bytes in size~~, with `PAGE_READWRITE` access. An entry should be present for each module that is loaded, regardless of whether the module is loaded from disk or not.

If you want to validate this for yourself, use Cheat Engine to scan an injected process for the following groupscan: `8:0 8:0 8:0 8:0 8:0 8:0 8:* 8:* 8:1 8:2 8:0 8:2 8:0 8:2`.  You can then look in the memory viewer (set the display type to 8 byte hex) and follow the pointer to see the module name strings.

The check is basically:

 - Look for all committed private memory regions in the current process ~~that are 0x20000 bytes in size~~, with both current and allocated protection set to `PAGE_READWRITE`
 - Scan each of these regions as follows:
   - Look for six leading zeroed QWORDs
   - Check if the top 32 bits of the next QWORD match the top 32 bits of the current address, and that the address here points to a different committed memory region that is also marked with read-write access.
   - Check that the next DWORD is zero.
   - Check that the next DWORD is less than MAX_PATH (this is the string length)
   - Check that the next QWORD is 1.
   - Check that the next three QWORD pairs are 2,0.

I can implement a check for this using the same approach that our existing memory scanner uses.

Update: The size isn't always 0x20000. However a good heuristic for finding the right memory region is that `AllocationBase` and `Base` point to different locations, both starting with QWORD 0, and `AllocationBase+0x10` is 0x00000002FFEEFFEE. Might need to experiment with this more.

Fix patch:
diff --git a/al-khaser/Al-khaser.cpp b/al-khaser/Al-khaser.cpp
index 2932c44..b83abc8 100644
--- a/al-khaser/Al-khaser.cpp
+++ b/al-khaser/Al-khaser.cpp
@@ -101,6 +101,7 @@ int main(void)
 		exec_check(&ScanForModules_LDR_Direct, TEXT("Enumerating the process LDR directly "));
 		exec_check(&ScanForModules_MemoryWalk_GMI, TEXT("Walking process memory with GetModuleInformation "));
 		exec_check(&ScanForModules_MemoryWalk_Hidden, TEXT("Walking process memory for hidden modules "));
+		exec_check(&ScanForModules_DotNetModuleStructures, TEXT("Walking process memory for .NET module structures "));
 	}
 
 	/* Generic sandbox detection */
diff --git a/al-khaser/AntiDebug/ScanForModules.cpp b/al-khaser/AntiDebug/ScanForModules.cpp
index b621cf1..898eee1 100644
--- a/al-khaser/AntiDebug/ScanForModules.cpp
+++ b/al-khaser/AntiDebug/ScanForModules.cpp
@@ -342,12 +342,14 @@ BOOL ScanForModules_MemoryWalk_Hidden()
 			if (GetModuleHandleEx(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, (TCHAR*)addr, &moduleHandle) == FALSE)
 			{
 				// not a known module
-				if ((region->Protect & PAGE_EXECUTE) == PAGE_EXECUTE ||
-					(region->Protect & PAGE_EXECUTE_READ) == PAGE_EXECUTE_READ ||
-					(region->Protect & PAGE_EXECUTE_WRITECOPY) == PAGE_EXECUTE_WRITECOPY ||
-					(region->Protect & PAGE_EXECUTE_READWRITE) == PAGE_EXECUTE_READWRITE)
+				if ((region->State & MEM_COMMIT) == MEM_COMMIT &&
+					((region->Protect == PAGE_READONLY) ||
+					(region->Protect == PAGE_READWRITE) || 
+					(region->Protect == PAGE_EXECUTE_READ) || 
+					(region->Protect == PAGE_EXECUTE_READWRITE) || 
+					(region->Protect == PAGE_EXECUTE_WRITECOPY)))
 				{
-					auto moduleData = static_cast<PBYTE>(region->AllocationBase);
+					auto moduleData = static_cast<PBYTE>(region->BaseAddress);
 					if (moduleData[0] == 'M' && moduleData[1] == 'Z')
 					{
 						if (firstPrint)
@@ -361,7 +363,7 @@ BOOL ScanForModules_MemoryWalk_Hidden()
 							}
 						}
 
-						printf(" [!] Executable at %p\n", region->AllocationBase);
+						printf(" [!] Executable at %p\n", region->BaseAddress);
 						anyBadLibs = true;
 					}
 				}
@@ -408,6 +410,90 @@ BOOL ScanForModules_MemoryWalk_Hidden()
 	return anyBadLibs ? TRUE : FALSE;
 }
 
+BOOL ScanForModules_DotNetModuleStructures()
+{
+	HMODULE moduleHandle = 0;
+	TCHAR moduleName[MAX_PATH];
+
+	auto memoryRegions = enumerate_memory();
+
+	bool anyBadLibs = false;
+
+	/*
+	This works because the .NET runtime loads structures into memory that describe modules. This happens even if the module is loaded dynamically, from memory.
+	If al-khaser were a .NET application we'd need to apply some additional checks on the results, but since it isn't then we can just report every .NET module we find.
+	This check is quite effective because it catches pretty much any kind of .NET injection, even if the injector uses tricks like messing with PE headers or patching EWT.
+	*/
+
+	bool firstPrint = true;
+	for (PMEMORY_BASIC_INFORMATION region : *memoryRegions)
+	{
+		if (region->State == MEM_FREE || region->Type == MEM_MAPPED || region->Type == MEM_IMAGE)
+		{
+			//printf("region %p skipped for being free, mapped, or image.\n", region->BaseAddress);
+			delete region;
+			continue;
+		}
+
+		if ((region->State & MEM_COMMIT) == MEM_COMMIT &&
+			region->Protect == PAGE_READWRITE &&
+			region->AllocationProtect == PAGE_READWRITE)
+		{
+			uint64_t* addr = static_cast<uint64_t*>(region->BaseAddress);
+			uint64_t* regionEnd = addr + (region->RegionSize / sizeof(uint64_t));
+
+			// check first qword at region base address. should be zero.
+			if (*addr == 0)
+			{
+				// find the pattern of QWORDs we want (0, 0, 0, 0, 0, 0, pointer, length, 1, 2, 0, 2, 0, 2, 0)
+				while (addr < regionEnd - 32)
+				{
+					uint64_t* ptr = addr;
+					bool sixZeroes = true;
+					for (int i = 0; i < 6; i++)
+					{
+						sixZeroes &= *(ptr++) == 0;
+					}
+					if (sixZeroes)
+					{
+						//printf("got six zeroes at %p\n", ptr);
+						uint64_t stringPtrVal = *ptr;
+						PCWSTR stringPtr = reinterpret_cast<PCWSTR>(*ptr);
+						ptr++;
+						uint64_t stringLen = *ptr;
+						ptr++;
+						if (*ptr++ == 1 && *ptr++ == 2 && *ptr++ == 0 && *ptr++ == 2 && *ptr++ == 0 && *ptr++ == 2 && *ptr++ == 0)
+						{
+							// pattern matches, check string addr
+							if ((stringPtrVal & 0xFFFFFFFF00000000ULL) == ((uint64_t)ptr & 0xFFFFFFFF00000000ULL))
+							{
+								// check string length is sane
+								if (stringLen < MAX_PATH * sizeof(wchar_t))
+								{
+									// ok, we're sure it's the right structure. report it.
+
+									if (firstPrint)
+									{
+										printf("\n\n");
+									}
+									printf(" [!] Found module: %S (structure address %p)\n", stringPtr, stringPtr);
+									anyBadLibs = true;
+								}
+							}
+						}
+					}
+					addr++;
+				}
+			}
+		}
+
+		delete region;
+	}
+	delete memoryRegions;
+
+	return anyBadLibs ? TRUE : FALSE;
+}
+
 std::vector<LDR_DATA_TABLE_ENTRY*>* WalkLDR(PPEB_LDR_DATA ldrData)
 {
 	auto entryList = new std::vector<LDR_DATA_TABLE_ENTRY*>();
diff --git a/al-khaser/AntiDebug/ScanForModules.h b/al-khaser/AntiDebug/ScanForModules.h
index 0da4582..8baede0 100644
--- a/al-khaser/AntiDebug/ScanForModules.h
+++ b/al-khaser/AntiDebug/ScanForModules.h
@@ -8,3 +8,4 @@ BOOL ScanForModules_LDR_Direct();
 BOOL ScanForModules_LdrEnumerateLoadedModules();
 BOOL ScanForModules_MemoryWalk_GMI();
 BOOL ScanForModules_MemoryWalk_Hidden();
+BOOL ScanForModules_DotNetModuleStructures();


Test patch: