Add-Type이 실행될 때 C# 코드가 메모리상에서 컴파일 된다고 위에서 얘기했었다. 더 자세하게 얘기하자면 csc.exe 를 이용해 메모리상에서 C# 코드를 컴파일 하지만, 그 와중에 임시 파일을 디스크위에 작성한다.
2022년도 기준 Add-Type 를 이용한 파워쉘에서의 C# 코드 및 윈도우 API 실행은 유명한 TTP라 왠만한 AV/EDR 솔루션들 및 윈도우 디펜더는 이를 기본적으로 막는다.
Dynamic Method
다음 코드들은 다음 두 개의 레퍼런스 - Matt Graeber, mez0, DepthSecurity 라는 분들의 코드 참고했다. 먼저 LookUpFunction 이라는 함수로 현 .NET AppDomain에 있는 어셈블리들 중 System.dll 닷넷 어셈블리를 찾고, 그 중 UnsafeNativeMethod 를 찾는다. 그 뒤, GetMethods() 함수를 통해 닷넷 어셈블리에서 GetProcAddress 와 GetModuleHandle 윈도우API를 찾는다. 그 뒤, 이들을 이용해 파라미터로 받아온 ModuleName과 FunctionName을 찾는다.
# Unmanaged DLL 이름과 winAPI함수 이름을 입력값으로 받고, 함수 포인터를 반환함.
function LookUpFunc {
Param($module, $funcName)
$assem = ([AppDomain]::CurrentDomain.GetAssemblies() | Where-Object { $_.GlobalAssemblyCache -And $_.Location.Split('\\')[-1].Equals('System.dll') }).GetType('Microsoft.Win32.UnsafeNativeMethods')
$GetProcAddress = $assem.GetMethod('GetProcAddress', [Type[]] @('System.Runtime.InteropServices.HandleRef', 'string'))
return $GetProcAddress.Invoke($null, @([System.Runtime.InteropServices.HandleRef](New-Object System.Runtime.InteropServices.HandleRef((New-Object IntPtr), ($assem.GetMethod('GetModuleHandle')).Invoke($null, @($module)))), $funcName))
}
결국 $fPointer = LookUpfunction Kernel32.dll VirtualAlloc 과 같은 파워쉘 코드를 이용하면 VirtualAlloc 의 위치를 가르키는 함수 포인터를 생성할 수 있게 된다.
함수 포인터가 있다고 해서 그것을 무작정 사용할 수는 없다. 함수 포인터가 가르키는 메모리는 어떻게 읽어야하는가? 몇개의 argument 가 있고, 어떤 타입의 argument 들이 있는가? 리턴하는 데이터 타입은? 에 대한 설명을 기반으로 함수 포인터를 실행해야 하는데, C#과 파워쉘에서는 이를 Delegate 을 통해서 해결한다.
다음은 파워쉘에서 Delegate 을 만들고 그것을 기반으로 위에서 받아온 함수 포인터를 실행하는 코드다. 다이내믹 어셈블리, 모듈, 타입, 프로토타입, 매써드 등을 만든 뒤 DelegateType을 반환한다.