Exploiting CVE-2024-37148
Intro When it comes to input sanitisation, who is responsible, the function or the caller ? Or both ? And if no one does, hoping that the other one will do t...
TL;DR
A few experiments about mixed managed/unmanaged assemblies. To begin with, we start by presenting a C# programme that hides a part of its payload in an unmanaged C++ stub as obj
file, and interact with. Loading it into dnSpy
will show that part of the code is unmanaged and cannot be displayed properly. We take it one step further by removing the references to the UNmanaged code and changing the EXE entry point. The programme then executes the unamanaged code and we leverage the power of ICLRRuntimeHost
to get back to the C# part. Since there is no reference to the unmanaged code, dnSpy
shows nothing but the C#.
As a lazy reverse engineer, facing a .NET binary would immediately make me fire up dnSpy
to dive into the code. Seeing something as simple as this would not interest me that much at first sight.
Only one namespace, one class, and two dummy routines. I would assume that the binary only prints Hey, I just met you, and this crazy, and quickly terminates. But by running it, I would see something really strange, with unexpected messages:
Wait, what ? 0.o Where do these strings comes from ?! Let’s dive into the fabulous world of mixed-mode assemblies, and be ready for a what-the-f***-is-this-shit-ness overload …
C# is a really handy language (still not my favourite, though, python 2.7 you’re still my love) but for malware authors, it has a significant drawback: it is fairly easy to reverse engineer. Tools such as dnSpy
are able to provide a source code version quite akin to the original one, and make analysis much easier than reading bits and bytes. Instead of being directly compiled to machine code, C# is translated into an intermediary language named CIL (formerly named IL, for … Intermediary Language), and then just-in-time compiled in order to run in the .NET environment.
The code is referred to as managed code, meaning that it expects to run under the management of a Common Language Infrastructure (CLI), such as .NET Framework or Common Language Runtime (CLR). Common Language Infrastructure is an open specification that describes how a runtime environment could allow multiple languages to be used on different computing platforms, without being rewritten. To rephrase it with simple words, managed code is just a code whose execution is handled by a runtime (CLR is this case), turning managed code into machine code, and executing it. The advantage of managed code is the interoperability between languages, and the fact that the burden of memory management is not left to the developer (well, not entirely at least).
On the other hand, the unmanaged code (written in C, C++ for instance), has the advantage that it is directly turned into machine language, and cannot be decompiled into meaningful words, compared to C#. C++ is not supposed to be managed, but the variant C++/CLI came as a means to better connect .NET Framework and C++. Simply said, C++/CLI is a version of C++, modified in order to run in CLI. It therefore provides ways to interact with other .NET languages, such as C#.
Knowing that C++ programs can call C functions, we therefore have a way to pass from C# to C …
To make a C++ library able to interact with .NET platform, one has to add the switch /clr
to the compiling command. This would make the module able to benefit from the .NET features, while still being compatible with the rest. While linking, another interesting flag, known as /CLRIMAGETYPE:IJW
, must be added. Also known as C++ Interop, this humorous flag stands for It Just Works. The first /clr
switch creates managed assembly, and /ijw
just automagically makes it usable, like any other managed class.
Normally, a meme with Todd Howard should have been put here, but Sponge Bob does the job
The first experiment was to build a C# programme with “hidden” features, that is, features that cannot be easily recovered with dnSpy
. To do so, I first created a CLR Class library (.NET Framework), with a first C++ class, with the feature I wanted to hide (in this example, executing the command whoami
).
Class UnmanagedLib.cpp
#include "UnmanagedLib.h"
void execwhoami() {
std::array<char, 128> buffer;
std::unique_ptr<FILE, decltype(&_pclose)> pipe(_popen("whoami", "r"), _pclose);
if (!pipe) return;
std::string outbuff;
while (fgets(buffer.data(), (int)buffer.size(),pipe.get()) != nullptr) {
outbuff += buffer.data();
}
puts(outbuff.c_str());
}
Header UnmanagedLib.h
#pragma once
#include <array>
#include <memory>
#include <string>
void execwhoami();
After that, I added a wrapper around this library, meant to be compiled with the famous /clr
flag. It would offer the possibility to call the function from C# while still making dnSpy
unable to properly decompile it. I then added a second class named CInterop
, and left the class empty (except the #include "CInterop.h"
directive. Also, the #include "pch.h"
should be removed, if any).
Header CInterop.h
#pragma once
#include "UnmanagedLib.h"
namespace ManagedConsoleApp {
ref class CInterop
{
public:
static void doExecWhoami() { execwhoami(); }
};
}
This piece of code wraps the native function inside doExecWhoami
, which will be made available to C#. The namespace ManagedConsoleApp
is used on purpose, because it is the one we are going to use in the managed code. This class CInterop
would then belong to the same namespace. Not mandatory to do it like this, though.
To compile it and create things usable by the C# programme, one better should compile manually (from Developer PowerShell in VisualStudio). The global idea is to compile individual components, and linking at the end. The following command would generate the object file UnmanagedLib.obj
. We compile without linking yet (/c
) and creates a multithreaded binary (/MD
) by using MSVCRT.lib
:
PS > cl.exe /c /MD .\UnmanagedLib.cpp
Microsoft (R) C/C++ Optimizing Compiler Version 19.36.32537 for x86
Copyright (C) Microsoft Corporation. All rights reserved.
UnmanagedLib.cpp
Now, time to compile CInterop
:
PS > cl.exe /clr /LN /MD .\CInterop.cpp .\UnmanagedLib.obj
Microsoft (R) C/C++ Optimizing Compiler Version 19.36.32537
for Microsoft (R) .NET Framework version 4.08.9167.0
Copyright (C) Microsoft Corporation. All rights reserved.
CInterop.cpp
Microsoft (R) Incremental Linker Version 14.36.32537.0
Copyright (C) Microsoft Corporation. All rights reserved.
/out:CInterop.netmodule
/dll
/noentry
/noassembly
CInterop.obj
.\UnmanagedLib.obj
This command would produce CInterop.netmodule
(switch /LN
) and CInterop.obj
. The three newly created files would be then used during compilation and linking time, to make the C# programme benefit from the function execwhoami
.
Let’s now create another project in VisualStudio, by choosing this time Console app (.NET Framework) as a template. The class Program.cs
is as follows, calling the wrapped execwhoami
. One can notice here that since CInterop
class belongs to the namespace ManagedConsoleApp
, there is no need to put something before:
namespace ManagedConsoleApp
{
internal class Program
{
static void Main(string[] args)
{
CInterop.doExecWhoami();
}
}
}
Let’s copy Cinterop.obj
, CInterop.netmodule
and UnmanagedLib.bj
in this folder. Although VisualStudio complains about this unknown name Cinterop
, csc.exe
should happily compile:
PS > csc.exe /target:module /addmodule:CInterop.netmodule .\Program.cs
Microsoft (R) Visual C# Compiler version 4.6.0-3.23259.8 (c3cc1d0c)
Copyright (C) Microsoft Corporation. All rights reserved.
This command instructs the compiler to build a module, adding a reference to Cinterop.netmodule
. Now it’s time to merge everything with link.exe
:
PS > link.exe /LTCG /CLRIMAGETYPE:IJW /ENTRY:ManagedConsoleApp.Program.Main /SUBSYSTEM:console /ASSEMBLYMODULE:Cinterop.netmodule .\CInterop.obj .\UnmanagedLib.obj .\Program.netmodule
Microsoft (R) Incremental Linker Version 14.36.32537.0
Copyright (C) Microsoft Corporation. All rights reserved.
Generating code
Finished generating code
The switches are as follows:
/LTCG
: perform whole-programme optimisation/CLRIMAGETYPE
: to make it “just working”/ENTRY
: the programme entry point/SUBSYSTEM
: kind of application: console, window, boot, EFI/ASSEMBLYMODULE
: add this module to the assemblyIt should generate a CInterop.exe
(which can be changed with the optional switch /OUT:name.exe
). Running it finally tells you who you are (:
Let’s load it into dnSpy
, and see what it tells:
However, by clicking on the symbol <Module>.execwhoami
, one reaches an impasse.
By doing some conversions, it is even possible to pass arguments between managed and unmanaged code. Just beware of calling conventions.
While this first example is interesting, a reverse engineer can easily see where something smells fishy, since they will clearly see the link to unmanaged code (although it might have legitimate reasons), and would guess that something may be hidden there. The second experiment takes it further by hiding the module from C#, and still bouncing between managed and unmanaged code. The idea here is to set the entry point to mainCRTStartup
, and implement a main
in the unmanaged code. Once executed, the goal is to “get back” to the managed code.
.NET EXE files contain in their Cor20 header a field named EntryPointTokenOrRva
. Let’s take a look at what the doc says (source: https://www.ntcore.com/files/dotnetformat.htm):
Depending of the value of Flags
, the entry point is considered as a native or a managed one. By taking a look at what dnSpy
says, one can see that the flags are clear. The entry point is therefore managed, and it makes sense since it’s Program.Main
.
To initialise the CRT, the routine mainCRTStartup
needs to be called to set everything up, call initialisers, and finally call the developer’s main
routine. It means that declaring a valid main
routine (that is, a routine with recognisable signature) in the unmanaged code, then setting the entry point to mainCRTStartup
, and our unmanaged routine will be called.
To begin with, let’s create a dummy C# console application, that contains nothing more than a Console.WriteLine
in its Program
class:
using System;
namespace EvilExe
{
public class Program
{
public static void Main(string[] args){
callMeMaybe("Hey, I just met you, and this is crazy");
}
public static int callMeMaybe(String msg)
{
Console.WriteLine(msg);
return 0;
}
}
}
Compiling it with VisualStudio recipe and loading the binary into dnSpy
, and one would only see this:
Running it would print Hey, I just met you, and this is crazy since Main
would be recognised as the entry point. Fair enough.
As mentioned earlier, the goal of the unmanaged code is to execute something unexpected and then to execute the legitimate code to make the analyst think that everything went well. To begin with, let’s create a new CLR Class library (.NET Framework) with a class named CInterop
. The header only contains one routine, that should be reachable from mainCRTStartup
:
Header CInterop.h
#pragma once
#include <cstdio>
#include <metahost.h>
#include <string>
#include <msclr\marshal_cppstd.h>
int __stdcall main(int argc, char** argv,char** env);
For the moment, let’s add a simple call to puts
in the main
Class CInterop.cpp
#include "CInterop.h"
using namespace System;
int __stdcall main(int argc, char** argv, char** env) {
puts("Parle à ma main");
//callCSharp();
return 0;
}
Individually compiling and linking as we did previously, while setting the entry point to mainCRTStartup
, should just print Parle à ma main and exit. The message Hey, I just met you, and this is crazy will not be printed since the EvilExe.Program.Main
will not be called.
For the C++ part:
PS> cl.exe /clr /c CInterop.cpp
Let’s copy the newly obtained obj
file and compile the C#:
PS> csc.exe /target:module Program.cs
PS> link.exe /LTCG /CLRIMAGETYPE:IJW /ENTRY:mainCRTStartup /OUT:EvilEXE.exe Cinterop.obj Program.netmodule
Since we did not add the switch addmodule
, there is no reason that it appears in dnSpy
, completely hiding it inside the binary.
However, the challenge is now to “return” to the C# code. Multiple possibilities exist, actually. The one I used here leverages the power of ICLRRuntimeHost
and friends. Let’s now implement the routine callCSharp
in the unmanaged code.
Thanks to C++/CLI, it is possible to use .NET classes inside C++ code. The technique is not new and even quite well documented (just an example: https://www.ired.team/offensive-security/code-injection-process-injection/injecting-and-executing-.net-assemblies-to-unmanaged-process. Or another one: https://0xpat.github.io/Malware_development_part_9/. Or yet another one: https://codingvision.net/calling-a-c-method-from-c-c-native-process).
Known as CLR Hosting (https://learn.microsoft.com/en-us/previous-versions/visualstudio/visual-studio-2008/zaf1h1h5(v=vs.90)), the trick is to host the .NET CLR in a process of our choice, in order to tweak it. As stated by the documentation, the process is transparent for applications that were meant to run in the CLR, the runtime being automatically started by mscoree.dll
. However, unmanaged applications can host the CLR to benefit from its capabilities and control its features according to their needs.
To begin with, an instance of the CLR is created:
void callCSharp() {
ICLRMetaHost* metaHost = NULL;
ICLRRuntimeInfo* runtimeInfo = NULL;
ICLRRuntimeHost* runtimeHost = NULL;
if (CLRCreateInstance(CLSID_CLRMetaHost, IID_ICLRMetaHost, (LPVOID*)&metaHost) != S_OK) return;
...
}
Once the host created, the runtime can be created inside. To do so, the routine is metaHost->GetRuntime(LPCWSTR pwzVersion, REFIID riid, LPVOID *ppRuntime)
. The first argument is supposed to be the version of the runtime. The latter can be obtained in C# through the property System::Environment::Version
, and to avoid hardcoding it in the binary, it should be computed at runtime. Conversions must be done to retrieve it:
...
System::String^ managed = System::Environment::Version->ToString();
std::string version = msclr::interop::marshal_as<std::string>(managed);
std::wstring temp = std::wstring(version.begin(), version.end());
LPCWSTR wideString = temp.c_str(); //4.0.30319.42000
...
Depending on the .NET framework version, the result may change. As stated in the doc (https://learn.microsoft.com/en-us/dotnet/api/system.environment.version?view=net-7.0):
For the .NET Framework Versions 4, 4.5, 4.5.1, and 4.5.2, the Environment.Version property returns a Version object whose string representation has the form 4.0.30319.xxxxx. For the .NET Framework 4.6 and later versions, and .NET Core versions before 3.0, it has the form 4.0.30319.42000.
However, the routine metaHost->GetRuntime
expects it as vXXXXX.YYYY.ZZZZ
(three values prepended with a 'v'
). Let’s write a dirty piece of code that builds a suitable version string (probably not the best, but I’m a Python guy, and C#/C++/C give me pimples). If everything went well so far, we can continue by getting a reference to the runtime host thanks to runtimeInfo->GetInterface
:
...
char shortversion[32] = { 0 };
shortversion[0] = 'v';
int count = 0;
for (int i = 0; wcslen(wideString); i++) {
if (wideString[i] == '.' && ++count == 3) { //if we reach the 3rd '.', then stop
break;
}
shortversion[i+1] = wideString[i];
}
wchar_t wtext[32];
mbstowcs(wtext, shortversion, strlen(shortversion) + 1);
if (metaHost->GetRuntime(wtext, IID_ICLRRuntimeInfo, (LPVOID*)&runtimeInfo) != S_OK) return;
if (runtimeInfo->GetInterface(CLSID_CLRRuntimeHost, IID_ICLRRuntimeHost, (LPVOID*)&runtimeHost) != S_OK) return;
...
The runtimeHost
could be started
and if running, the routine ExecuteInDefaultAppDomain
will be our way to call our C# routine. However, the routine called by ExecuteInDefaultAppDomain
has to stick to a specific signature, otherwise it would not be recognised:
virtual HRESULT STDMETHODCALLTYPE ExecuteInDefaultAppDomain(
/* [in] */ LPCWSTR pwzAssemblyPath,
/* [in] */ LPCWSTR pwzTypeName,
/* [in] */ LPCWSTR pwzMethodName,
/* [in] */ LPCWSTR pwzArgument,
/* [out] */ DWORD *pReturnValue) = 0;
One can better understand why callMeMaybe
returns a useless 0, now. It must return an integer.
Since the first argument to ExecuteInDefaultAppDomain
is the DLL or EXE to load, we must first retrieve it thanks to GetModuleFileNameW
:
...
DWORD pReturnValue;
WCHAR szExeFileName[MAX_PATH];
GetModuleFileNameW(NULL, szExeFileName, MAX_PATH); //get current binary name
HRESULT res = runtimeHost->ExecuteInDefaultAppDomain(szExeFileName, L"EvilExe.Program", L"callMeMaybe", L"Here's my number: <censored>", &pReturnValue);
if (res == S_OK)
{
puts("CLR executed successfully\n");
}
else {
printf("Error code: %d\n", res);
}
}
And that’s it! Let’s compile, run, and admire the result:
Intro When it comes to input sanitisation, who is responsible, the function or the caller ? Or both ? And if no one does, hoping that the other one will do t...
Intro After being tasked with auditing GLPI 10.0.12, for which I uncovered two unknown vulnerabilities (CVE-2024-27930 and CVE-2024-27937), I became really i...
Intro A few weeks ago, I discovered during an intrusion test two vulnerabilities affecting GLPI 10.0.12, that was the latest public version at this time. The...
I was recently tasked with auditing the application GLPI, a few days after its latest release (10.0.12 at the time of writing). The latter stands for Gestion...
I won’t insult you by explaining once again what JSON Web Tokens (JWTs) are, and how to attack them. A plethora of awesome articles exists on the Web, descri...
A few days ago, I published a blog post about PHP webshells, ending with a discussion about filters evasion by getting rid of the pattern $_. The latter is c...
A few thoughts about PHP webshells …
I remember this carpet, at the entrance of the Computer Science faculty, with this message There’s no place like 127.0.0.1/8. A joke that would create two ca...
TL;DR A few experiments about mixed managed/unmanaged assemblies. To begin with, we start by presenting a C# programme that hides a part of its payload in an...
It was a sunny and warm summer afternoon, and while normal people would rush to the beach, I decided to devote myself to one of my favourite activities: suff...
The reader should first take a look at the articles related to CVE-2023-3032 and CVE-2023-3033 that I published a few days ago to get more context.
This walkthrough presents another vulnerability discovered on the Mobatime web application (see CVE-2023-3032, same version 06.7.2022 affected). This vulnera...
Mobatime offers various time-related products, such as check-in solutions. In versions up to 06.7.2022, an arbitrary file upload allowed an authenticated use...
King-Avis is a Prestashop module developed by Webbax. In versions older than 17.3.15, the latter suffers from an authenticated path traversal, leading to loc...
Let’s render unto Caesar the things that are Caesar’s, the exploit FuckFastCGI is not mine and is a brilliant one, bypassing open_basedir and disable_functio...
I have to admit, PHP is not my favourite, but such powerful language sometimes really amazes me. Two days ago, I found a bypass of the directive open_basedir...
PHP is a really powerful language, and as a wise man once said, with great power comes great responsibilities. There is nothing more frustrating than obtaini...
A few weeks ago, a good friend of mine asked me if it was possible to create such a program, as it could modify itself. After some thoughts, I answered that ...
In the previous article, I described how I wrote a simple polymorphic program. “Polymorphic” means that the program (the binary) changes its appearance every...
The malware presented in this blog post appeared on Google Play in 2016. I heard about it thanks to this article published on checkpoint.com. The malicious a...
Ransomwares are really interesting malwares because of their very specific purpose. Indeed, a ransomware will not necessarily try to be stealth or persistent...
A few days ago, I found this article about a malware targeting Sberbank, a big Russian bank. The app disguises itself as a web application, stealing in backg...
RuMMS is a malware targetting Russian users, distributed via websites as a file named mms.apk [1]. This article is inspired by this analysis made by FireEye ...
Could a 5-classes Android app be so harmful ? dsencrypt says “yes”…
~$ cat How_an_Android_app_could_escalate_its_privileges_Part4.txt
~$ cat How_an_Android_app_could_escalate_its_privileges_Part3.txt
~$ cat How_an_Android_app_could_escalate_its_privileges_Part2.txt
~$ cat How_an_Android_app_could_escalate_its_privileges.txt
Even if the thesis introduces the extensions internals, and analyses the difference between mobile and desktop browsers in terms of likelihood, efficiency an...