Buenas a todos, en este articulo voy a escribir acerca de un mini proyecto el cual me ha llamado la atención al hacer una auditoria de un binario .NET. Os cuento la situación
El otro día me tocó una auditoria de un software escrito en .NET que se comunicaba con un servidor. El punto es que ese binario tenía SSL Pinning por lo que no podía usar Burp a no ser que lo saltara.
Uno en este punto puede pensar: David es un .NET, decompilalo con DNSpy, parchea la función, recompilalo y ya, no te buscas mas historias xD Y en parte tienes razón, pero…. ¿Y si no fuera tan fácil de encontrar porque el código del binario esta ofuscado o es tan grande que no tienes tiempo de buscar donde está el Pinning? Es aquí donde entra mi investigación xd
En este articulo cubriremos el pinning realizado con la documentación de Microsoft que son con las clases HttpClient y HttpClientHandler, desconozco si este metodo funciona con otro tipo de librerias de terceros.
Dicho eso primero hay que ver como funciona el SSL Pinning con HttpClient
Desarrollando un SSLPinning
Realizar un SSL Pinning en .NET es relativamente sencillo, basta con usar la clase HttpClientHandler e instanciarla como un objeto.
Luego hay que crear una funcion XXXXX que será la función donde programaremos el SSL Pinning, esta funcion devolverá True si todo está OK y False si hay algo raro en el certificado.
Una vez asignada la funcion al callback de HttpClientHandler, añadimos el objeto al HttpClient y a partir de este momento, todo GET, POST, PUT etc, pasará por una validación de certificado (SSL Pinning)
Para que os quede claro os pongo el código a continuación, veréis que esta explicación es mucho texto para lo poco que es el código xD
private void Form1_Load(object sender, EventArgs e)
{
handler = new HttpClientHandler();
handler.ServerCertificateCustomValidationCallback = ValidateServerCertificate;
client = new HttpClient(handler);
}
public static bool ValidateServerCertificate(HttpRequestMessage requestMessage, X509Certificate2 certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
{
return !certificate.IssuerName.Name.Contains("PortSwigger");
}
private void button1_Click(object sender, EventArgs e)
{
try
{
var response = client.GetAsync("<https://blog.davidc96.com>");
MessageBox.Show(response.Result.StatusCode.ToString());
}
catch (Exception ex)
{
MessageBox.Show(ex.InnerException.ToString());
}
}
Cuando el Pinning de False, HttpClient creará una excepcion que capturamos en el Try Catch

En este punto, he creado un SSL Pinning muy sencillo que comprueba que el certificado no sea de Burp Suite (PortSwigger), no es el mejor Pinning del mundo pero me sirve xd

Los entresijos detrás del SSL Pinning
Una vez desarrollado el SSL Pinning, vamos a averiguar que hay detrás de este, como funciona y que Dlls de Microsoft están implicadas. Para eso, usaremos DNSpy y su debugger ya que el debugger de Visual Studio no me deja depurar las funciones que están en System.dll xd

Primero vamos a partir de una base con lo que hemos programado antes de revisar toneladas de código.
Un buen punto sería poner un breakpoint al return de la función que hace SSLPinning y ver quien ha llamado a dicha función.
Vamos con dicho y lo que vamos a hacer es revisar por cuales funciones pasa el resultado.
Después de seguir todo el arbol llegamos a la función VerifyRemoteCertificate. ¿Porque esta función?
El punto aqui es que a medida que has ido viendo el flujo desde nuestra función Callback hasta la función que yo os he mencionado, todo el resto de funciones intermedias tenian la palabra Callback y esta es la primera que parece que no tiene Callback en su nombre.
En resumen esto es todo el flujo desde nuestra función personalizada hasta la función que nos interesa es el siguiente:

Ya tenemos la función que nos interesa!!! Ahora molaría que pudiéramos hacer hooking a esta función y devolver siempre True sin importar lo que diga el resto xd
Hooking de funciones .NET con Harmony
Antes de continuar, vamos a definir un poco el termino hooking porque en .NET no funciona exactamente como un hooking tradicional.
Un hooking consiste en modificar el flujo de una función dentro de una Dll para que ejecute antes una función controlada por nosotros. Para ello modificamos el código original de la función a hookear para añadir un jumper a nuestra función.
En cambio en .NET todo esto se simplifica drasticamente al ser código interpetado, esto se hace con Reflection.
Por decirlo así, tu puedes decirle a un .NET de manera gratuita que a partir de ahora todo lo que llame a la función X pasará por mí función primero o cuando finalice, que el resultado pase por mi función. Esto es porque, por cada llamada que se hace a una función .NET, se ejecuta antes un Prefix y al finalizar un Postfix. En resumen, es como si el hooking viniera implementado por defecto de manera nativa en .NET xd

Para hacer el “hooking” de manera mas fácil usaremos Harmony ya que facilita múcho todo el proceso.
Harmony, entre otras cosas nos permite decidir cuando queremos ejecutar nuestra función. Si queremos ejecutarla antes de que se llame a la función (Para manipular parametros o poner controles) o después de ejecutar la función (para manipular resultados)
En nuestro caso, nos interesa manipular el resultado de la función para que siempre devuelva True por lo que haremos un Postfix.
Tampoco quiero entrar mucho en detalles sobre como se programa y demás, al final si os interesa podeis ir a mi Github y ver el código que realiza la magia. Aquí solo remarcaré aspectos clave que han sido posible este hooking
.NET y sus sistemas de visualización de clases y funciones
Claro, es importante saber que a pesar de que se puede hacer Reflection, no es lo mismo hacerlo a una clase publica que una privada y encima que esa clase no es que sea privada si no que es interna tambien xD.
Esto dificulta mucho el buscar la función que queremos hookear pero como bien he dicho la técnica de Reflection está soportada de manera nativa en .NET Framework así que va a ser engorroso pero no desafiante xd.
Lo primero que tenemos que hacer para encontrar la función a hookear, al ser privada e interna, necesitamos el nombre completo del Path de la DLL + las clases (QNAME). Algo así como este string.
"System.Net.Security.SecureChannel, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
Claro la pregunta es, de donde narices saco este String XD
Pues aquí toca un poco de mágia:
- Lo primero que hay que hacer es cargar toda la DLL padre y empezar con una clase que si sea pública y visible para todos y que use la función que queremos buscar, en este caso System.Uri
- Una vez cargado, buscamos en todas las clases internas el string System.Net.Security.SecureChannel
- Obtenemos el QNAME de esa clase interna
Y ya está, honestamente es super sencillo y no hay que iterar ni hacer cosas raras xD
private static string GetSysInternalAssemblyQualifiedName(string internalTypeName)
{
try
{
// First, let's find parent DLL, on this case is System.dll and then use Uri as an Entrypoint to find what we want.
Assembly systemAssembly = typeof(System.Uri).Assembly;
// Because it's internal, we need to use GetType to get the propertyu.
Type internalClassType = systemAssembly.GetType(internalTypeName);
if (internalClassType != null)
{
// When we have it, we can now obtain it
string assemblyQualifiedName = internalClassType.AssemblyQualifiedName;
return assemblyQualifiedName;
}
else
{
Console.WriteLine($"[-] Cannot find {internalTypeName} in {systemAssembly.GetName()}");
}
}
catch (Exception ex)
{
Console.WriteLine($"[EXCEPTION!!] Something goes wrong: {ex.Message}");
}
return "";
}
Utilizando Harmony para parchear la función VerifyRemoteCertificate
Una vez obtenido el QNAME, con la función GetType, podemos acceder a todos los metodos de la clase SecureChannel
var harmony = new Harmony("com.davidc96.ssl.pinning.bypass");
Type secureChannelType = Type.GetType(Constants.SSLBYPASS_DLL_QNAME);
var originalMethod = secureChannelType.GetMethod(Constants.SSLBYPASS_FUNCTION, BindingFlags.Instance | BindingFlags.NonPublic);
if (originalMethod == null)
{
// If method is not found, maybe Microsoft change the function name, better look it manually
Log("[SSLBypass] Cannot Find VerifyRemoteCertificate... Maybe the name has changed in a new update System.Net.Security.SecureChannel");
Log("[SSLBypass] It is recommended to use Utility tool to find the correct function name and change it in Constants.cs");
return;
}
// Get postfixMethod to patch it
var postfixMethod = new HarmonyMethod(typeof(SecureChannelPatches), nameof(SecureChannelPatches.VerifyRemoteCertificate_Postfix));
harmony.Patch(originalMethod, postfix: postfixMethod);
Fijaos que sencillo es el código para cargar Harmony. Buscamos la clase con Reflection y le decimos a harmony, usa un postfix aquí y ya XD
internal static class SecureChannelPatches
{
internal static void VerifyRemoteCertificate_Postfix(ref bool __result)
{
if (!__result)
{
PatchSSL.Log("[SSLBypass] Patching results!");
__result = true;
}
}
}
El SecureChannelPatches simplemente contiene la función Postfix y forzará el resultado __result a True y ya
Ahora toca probarlo, vamos a ir al código original de nuestra demo técnica, y vamos a incluir la libreria y llamar a la función y…

Me sale el mensajito de OK y no me sale la Excepción por lo que el parche se ha aplicado correctamente 🙂
Pero claro, esto es trampa ya que estoy llamando directamente a la DLL dentro del código y claro, en un caso real, nosotros no tenemos acceso al código. Efectivamente tienes razón por lo que vamos a intentar inyectar esta DLL a un proceso remoto
Inyectando nuestra DLL con EasyHook pero…
Lo ideal sería inyectar la DLL de manera remota con un inyector pero estamos en las mismas, no es lo mismo inyectar una DLL en C++ que una en .NET ya que es mucho mas sencillo xD. Es por eso que usaremos EasyHook.
Implementando una clase nueva como esta ya tendriamos la DLL lista para ser inyectada
public class DllEntry : IEntryPoint
{
public DllEntry(RemoteHooking.IContext context, string channelName)
{
// Nothing
}
// Method used by EasyHook to run de Dll
public void Run(RemoteHooking.IContext context, string channelName)
{
PatchSSL.Apply();
}
}
Ahora creariamos un proyecto nuevo y llamando a esta función y el PID del proceso, ya tendríamos la DLL Inyectada
RemoteHooking.Inject(
targetPid, // PID
injectionLibrary, // DLL to inject
null,
"" // Optional String
);
Al realizarlo y probarlo…

¡Vaya por dios! Pero si esto funcionaba al llamarla directamente… ¿Porque no funciona ahora?
Después de investigar mucho me di cuenta de lo que estaba pasando y en verdad tiene mas sentido de lo que parece XD.
Durante el post he demostrado que inyectar código externo a un binario .NET es relativamente fácil (para no decir que viene de manera nativa xd) peeeero Microsoft pensó en que eso podría ser inseguro por lo que implementó lo siguiente:
Cuando tu ejecutas un programa .NET junto a sus DLL se reserva un espacio para que todos puedan convivir, tu en ese espacio si eres de su secta, puedes hacer lo que te de la gana: Parchear, ejecutar, Hookear etc. en tiempo real.

Ahora bien, las cosas son diferentes para aquellos que vienen nuevos xD. En este caso .NET Framework lo que hará es dejar que se ejecute la DLL dentro del binario peeeero te va a aislar con el fin de que si manipulas algo, solo tu tendrás las modificaciones y no el binario.

Como podemos ver en la imagen, cada cosa tiene su espacio y da igual que nuestra DLL parchee el SSL Pinning, que no afectará al binario.
Para ello lo que hay que hacer es poder unificar los dos espacios de memoria y es aquí donde entra la magia del .NET porque con este maravilloso lenguaje, podemos ejecutar los binarios como si fueran funciones XD, es decir, podemos crear un programa en C# que llame a NuestroBinario.Main() en un hilo a parte y ejecutar así el programa
Desarrollando un Launcher de nuestro binario .NET
Claro, pensadlo de esta manera, si nosotros creamos un programa que llame directamente al Main() del binario .NET, podemos llamar nuestra DLL como si fuera una dependencia más, aplicamos el parche que queremos y ejecutamos la función Main en un hilo a parte pero todo dentro del mismo espacio de memoria.

¡David esto es imposible! Pues creeme si es posible con Reflection XD y con las funciones Assembly jaja
// Load Binary as an Assembly (Like Reference)
Assembly targetAssembly = Assembly.LoadFrom(targetExePath);
// Find the EntryPoint
MethodInfo targetMain = targetAssembly.EntryPoint;
if (targetMain == null)
{
Console.WriteLine("[ERR] Cannot find the entryPoint MAIN");
Console.ReadKey();
return;
}
ParameterInfo[] parameters = targetMain.GetParameters();
object[] argumentsToPass;
if (parameters.Length == 0)
{
argumentsToPass = null;
}
else
{
argumentsToPass = new object[] { new string[0] };
}
Console.WriteLine("[+] Creating a thread to execute the binary main function....");
// Let's create a Thread which will execute our Main function
Thread workerThread = new Thread(() =>
{
try
{
// Invoke the entrypoint with the arguments.
targetMain.Invoke(null, argumentsToPass);
}
catch (Exception ex)
{
Console.WriteLine($"[EXCEPTION] Failed to create a thread: {ex.InnerException}");
}
});
workerThread.IsBackground = true; // Send a thread into the background to avoid being closed
workerThread.Start(); // Init Thread
Con este fragmento de código se puede lanzar un .exe como si fuera una libreria, ahora falta ejecutarlo y ver que pasa.

Efectivamente!! Ya lo hemos conseguido, hemos conseguido parchear el binario y ya pasa todo por Burp!!!
Referencias y hacerlo más Universal
Para hacer el código mas universal, he hecho varias herramientas y modificaciones para cubrir otros casos:
- Como estamos modificando directamente las librerias del Sistema, estas pueden ser propensas a cambiar en un futuro. Para ello he desarrollado otra herramienta llamada MethodFinder que sirve para encontrar tanto el QNAME como las posibles funciones que pueden ser que se usen para verificar el certificado. Igualmente es recomendable usar DNSpy para ello

2. Para esta demo, he hardcodeado todo, pero en el resultado final en mi Github, vosotros podeis pasar directamente por parametros el binario que querais parchear al launcher y listos
Puntos a mejorar del proyecto:
- Probarlo con varios binarios .NET Framework: Al final se ha usado un caso sencillo, en programas mas complejos puede que no acabe de funcionar
- Hacerlo mas universal: Al final esto no deja de ser una aproximación a MelonLoader (https://melonwiki.xyz/#/) pero con menos cosas. El problema es que MelonLoader solo es compatible para juegos escritos en UNITY. La idea es hacerlo lo mas parecido a este.
Dejo el link del proyecto aquí: https://github.com/Davidc96/dotNET-SSL-Pinning-Bypass/tree/master
Muchísimas gracias por todo y nos vemos en un siguiente post.
Hasta Otra