
RW/NoAccess와 RX 사이에서 셸코드의 메모리 보호를 전환한 다음 그 내용을 암호화/복호화하는 고급 인메모리 회피 기법
셸코드의 내용을 주기적으로 암호화하고 복호화하여 RW(또는 NoAccess)와 RX 메모리 보호 상태 사이를 오가도록 만드는 또 다른 인메모리 회피 기법의 PoC 구현체입니다.
셸코드가 RW 또는 NoAccess 메모리 페이지에 상주하면 Moneta나 pe-sieve 같은 스캐너가 이를 추적하거나 덤프하여 분석할 수 없게 됩니다.
ThreadStackSpoofer를 릴리스한 후 README의 다음 항목에 대한 질문을 몇 가지 받았습니다.
슬립 전에 Beacon의 메모리 페이지 보호를 (RX/RWX에서) RW로 변경하고 내용을 암호화하세요. (Moneta나 pe-sieve 같은 스캐너를 회피할 수 있습니다)
그 전까지 저는 커뮤니티에서 이미 페이로드를 암호화/복호화하고 메모리 보호를 전환하여 비정상적인 실행 영역을 찾는 메모리 스캐너를 회피하는 방법을 알고 있다고 확신했습니다. 하지만 질문이 그렇지 않다는 것을 증명했기에, 저는 또 다른 회피 전략을 문서화하고 커뮤니티가 활용할 수 있는 샘플 구현을 제공하기 위해 이 무기화되지 않은 PoC를 공개하기로 결정했습니다.
이 PoC는 공격 커뮤니티에 이미 알려진 비교적 단순한 기법을 시연한 것입니다. (그래서 제가 정말 새로운 것을 가져온 것은 아닙니다.) 앞서 언급한 두 메모리 스캐너를 대상으로 회피 능력을 보여주는 일부 상용 프레임워크가 보여주는 마법 뒤에 숨은 비밀을 공개하고자 하는 바람에서입니다.
RW로 전환할 때의 비교는 다음과 같습니다. (또 다른 옵션은 PAGE_NOACCESS로 전환하는 것입니다. 아래에 설명되어 있습니다.)

이 구현은 제 ThreadStackSpoofer와 함께 상용 C2 제품이 제공하는 기능을 따라잡을 수 있는 샘플 구현을 Offensive Security 커뮤니티에 제공하며, 우리 Red Team 도구에서도 그에 못지않은 성능을 낼 수 있도록 합니다. 💪
이 프로그램은 셸코드 자가 주입을 수행합니다. (대략적인 과정은 고전적인 VirtualAlloc + memcpy + CreateThread 방식입니다.)
셸코드가 실행되면 (이 구현은 특히 Cobalt Strike Beacon 임플란트를 대상으로 합니다) Beacon이 잠들 때 kernel32!Sleep을 가로채는 Windows 함수가 후킹됩니다.
후킹된 MySleep 함수가 호출될 때마다 해당 메모리 할당의 경계를 찾아 보호를 RW로 전환한 다음 모든 바이트를 xor32로 암호화합니다.
예정된 시간만큼 대기한 후, 셸코드가 다시 우리의 MySleep 핸들러로 돌아오면 셸코드 데이터를 복호화하고 보호 상태를 RX로 되돌립니다.
PAGE_READWRITE로의 변동 방식kernel32!Sleep을 우리 콜백을 가리키도록 후킹합니다.VirtualAlloc + memcpy + CreateThread를 통해 셸코드를 주입하고 실행합니다. ThreadStackSpoofer에서와 달리 여기서는 셸코드를 실행하기 위해 ntdll의 어떤 것도 후킹하지 않고 우리 자신의 함수에서 점프합니다. 이는 변조된 ntdll 메모리를 가리키는 단순한 IOC를 메모리에 남기지 않으려는 시도입니다.MySleep 콜백이 호출됩니다.RW로 전환됩니다.kernel32!Sleep을 언후킹하여 Sleep이 트램폴린(in-line hooked)되었다는 단순한 IOC가 메모리에 남지 않도록 합니다.::Sleep을 호출합니다.RX로 되돌린 다음 kernel32!Sleep을 다시 후킹하여 이후의 슬립도 가로챌 수 있도록 합니다.PAGE_NOACCESS로의 변동 방식kernel32!Sleep을 우리 콜백을 가리키도록 후킹합니다.VirtualAlloc + memcpy + CreateThread를 통해 셸코드를 주입하고 실행합니다 ...MySleep 콜백이 호출됩니다.PAGE_NOACCESS로 전환됩니다.kernel32!Sleep을 언후킹하여 Sleep이 트램폴린(in-line hooked)되었다는 단순한 IOC가 메모리에 남지 않도록 합니다.::Sleep을 호출합니다.kernel32!Sleep을 다시 후킹하여 이후의 슬립도 가로챌 수 있도록 합니다.RX로 되돌린 뒤 셸코드 실행이 재개됩니다.이 기법은 완전히 새로운 것도 아니고, 제가 직접 고안한 것도 아닙니다. 단지 이 개념과 실제 활용법을 보여주는 구현일 뿐이며, 우리 Offensive Security 커뮤니티가 상용 C2 프레임워크가 제공하는 기능을 따라잡을 수 있도록 한 것입니다.
사실 저는 몇 년 전 Josh Lospinoso 님이 만든 놀라운 Gargoyle 작업을 통해 셸코드 메모리 보호를 전환하는 아이디어를 접하게 되었습니다.
관련 배경 자료는 다음과 같습니다.
Gargoyle은 VirtualProtect을 호출하는 ROP 시퀀스를 활용하여 자기 인식 및 자기 변동 셸코드라는 개념을 한 단계 더 발전시킵니다.
하지만 이 기법은 인상적이지만, Cobalt Strike의 Beacon과 함께 사용하려면 스레드를 종료하고 메모리에서 Beacon을 계속 다시 초기화해야 하기 때문에 적용하기가 그만큼 어렵습니다.
이 방식은 완벽하지는 않지만, 우리는 이미 자체 주입 로더 프로세스의 기반 위에서 작동하고 있으므로 셸코드가 작동하는 환경으로 원하는 모든 것을 할 수 있고 원하는 대로 숨길 수 있습니다. 이 기법(그리고 이전의 ThreadStackSpoofer)은 이러한 방식으로 셸코드를 실행함으로써 얻을 수 있는 이점을 보여줍니다.
PAGE_NOACCESS로 변동하는 구현은 ORCA666 님이 자신의 https://github.com/ORCA666/0x41 인젝터에서 선보인 작업에서 영감을 받았습니다.
그는 다음을 보여주었습니다.
이 구현에는 해당 아이디어가 포함되어 있으며, <fluctuate>에서 옵션 2로 사용할 수 있습니다.
그의 다른 프로젝트도 꼭 확인해 보시기 바랍니다.
ShellcodeFluctuation 도구는 세 개의 매개변수를 받습니다. 첫 번째는 셸코드 경로이고 두 번째는 기능 수정자입니다.```
Usage: ShellcodeFluctuation.exe
:
-1 - Read shellcode but dont inject it. Run in an infinite loop.
0 - Inject the shellcode but don't hook kernel32!Sleep and don't encrypt anything
1 - Inject shellcode and start fluctuating its memory with standard PAGE_READWRITE.
2 - Inject shellcode and start fluctuating its memory with ORCA666's PAGE_NOACCESS.
### Moneta (겉보기에는) 오탐```
C:\> ShellcodeFluctuation.exe beacon64.bin -1
먼저 Moneta64 스캐너가 별다른 의심스러운 동작 없이 그저 무한 루프를 실행하는 프로세스에 대해 어떻게 판단하는지 살펴보겠습니다:

보시다시피 일부 **오탐(false positive)**이 있습니다(적어도 제가 보기에는 그렇습니다). Mismatching PEB module / Phantom image를 감지한 것으로 보입니다.
메모리 경계는 ShellcodeFluctuate.exe 모듈 자체를 가리키며, 이 모듈이 MEM_IMAGE 유형임에도 불구하고 프로세스의 PEB에 연결되어 있지 않음을 나타낼 수 있습니다. 이는 일반적이지 않으며 상당히 이상하게 들립니다.
이 IOC의 원인은 저도 알지 못하며, 더 잘 이해하려 시도하지도 않았습니다. 하지만 실제로 크게 걱정할 만한 사항은 아닙니다.
이 탐지의 원인을 아시는 분이 있다면 정말 궁금합니다! 꼭 연락 주세요.
C:> ShellcodeFluctuation.exe beacon64.bin 0
두 번째 사용 사례는 우리 프로세스 내에서 작동하는 Beacon의 메모리 IOC를 제시합니다. 이는 맞춤형 `Artifact Kits`나 `User-Defined Reflective Loaders`(예: 제 [`ElusiveMice`](https://github.com/mgeeky/ElusiveMice))를 전혀 사용하지 않으며, 결과를 망칠 수 있는 초기 작업도 수행하지 않습니다.

`Moneta64`가 우리 shellcode가 있는 위치를 가리키는 `Abnormal private executable memory`를 올바르게 인식하는 것을 볼 수 있습니다.
이는 자동화 스캐너가 우리 shellcode를 덤프하고 분석할 수 있도록 노출하는 매우 강력한 메모리 IOC입니다. 좋지 않네요.
### RW 보호 기능이 있는 암호화된 Beacon```
C:\> ShellcodeFluctuation.exe beacon64.bin 1
이제 이 구현의 관점에서 가장 흥미로운 세 번째 사용 사례는 변동하는 Beacon입니다.

다소 오탐 으로 간주되는 첫 번째 IOC를 제외하면, kernel32.dll 메모리가 수정되었음을 가리키는 새로운 IOC가 보입니다.
하지만 이번에는 Abnormal private executable memory IOC가 없습니다. 우리의 변동(반복적인 암호화/복호화 및 메모리 보호 전환)이 활성화되어 있는 상태입니다.
그리고 참고로, pe-sieve는 /data 3 옵션을 사용하면 주입된 PE도 탐지합니다(이 옵션이 지정되지 않으면 탐지되지 않습니다):

현재 제 추측은 PE-Sieve가 Moneta와 동일한 특성을 포착하고 있다는 것입니다(아래의 kernel32.dll의 수정된 코드 에서 설명됨) - PE 매핑 모듈의 작업 세트(Working set)가 비어 있지 않다는 사실은 일종의 코드 인젝션이 있었다는 명백한 증거입니다. 그 특성은 Implanted PE / Implanted 로 표시됩니다. 만약 그렇다면, 결론은 Moneta의 관찰과 유사합니다. 탐지 측면에서 그 IOC에 대해 그다지 신경 쓸 필요는 없다고 생각합니다.
현재로서는 셸코드 실행을 중간에 가로챌 더 나은 방법을 생각해내지 못했습니다(Cobalt Strike를 기준으로 말하면), kernel32!Sleep을 후킹하는 것 외에는요. 따라서 우리는 이런 종류의 IOC를 남겨둘 수밖에 없습니다.
하지만 이봐요, 파일 시스템에 있는 그대로(C:\Windows\System32\kernel32.dll)와 비교해 여전히 어떤 바이트도 다르지 않고, 어떤 함수도 후킹되어 있지 않습니다. 도대체 어떻게 된 걸까요? 😉
C:> ShellcodeFluctuation.exe beacon64.bin 2

그렇게 하면 셸코드가 `RX` 페이지와 `NA` 페이지 사이를 효과적으로 오가게 됩니다.
현재로서는 `PAGE_READWRITE` 대신 `PAGE_NOACCESS`로 전환하는 것의 이점이 확실하지 않습니다.
### kernel32.dll에서 수정된 코드
그렇다면 수정된 `kernel32` IOC는 어떨까요?
이제 이 IOC의 실체를 파악하고 여기서 무슨 일이 벌어지고 있는지 알아보겠습니다.
먼저 언급된 메모리 영역, 즉 `kernel32.dll`의 `.text`(코드) 섹션을 덤프하겠습니다. 이를 위해 공개적으로 알려져 있고 안정적인 도구인 `ProcessHacker`를 사용하겠습니다:

수정되었다고 알려진 kernel32의 코드 섹션을 덤프하고, 해당 영역을 수정하지 않은 프로세스에서 실행 중인 kernel32에 대해서도 동일한 작업을 수행합니다.
두 개의 덤프를 확보한 후, 제 [expdevBadChars](https://github.com/mgeeky/expdevBadChars)를 사용하여 바이트 단위로 비교해 불일치 항목을 찾을 수 있습니다:

그 결과 서로 일치함을 확인할 수 있습니다. `kernel32.dll`에서 단 하나의 바이트도 수정되지 않았으며, 그 이유는 `kernel32!Sleep`을 호출하기 전에 후킹을 해제하고 있기 때문입니다:
`main.cpp:31:````
HookTrampolineBuffers buffers = { 0 };
buffers.originalBytes = g_hookedSleep.sleepStub;
buffers.originalBytesSize = sizeof(g_hookedSleep.sleepStub);
//
// Unhook kernel32!Sleep to evade hooked Sleep IOC.
// We leverage the fact that the return address left on the stack will make the thread
// get back to our handler anyway.
//
fastTrampoline(false, (BYTE*)::Sleep, &MySleep, &buffers);
// Perform sleep emulating originally hooked functionality.
::Sleep(dwMilliseconds);
IOC가 트리거되는 원인은 무엇일까요? Moneta를 자세히 살펴봅시다:

MODIFIED_CODE IOC를 보고하는 104번째 줄 부근의 Moneta Ioc.cpp에 진입하여, kernel32 풀을 분석하는 정확한 순간을 더 잘 드러내도록 코드를 약간 수정할 수 있습니다.
이제:
a = truekernel32에 b = 0x1000의 프라이빗 바이트가 있음을 볼 수 있습니다. 어떻게 된 일일까요? 원래는 0이어야 합니다.a && b) IOC가 보고됩니다.Windows 이미지 로더가 DLL 모듈을 프로세스 메모리 공간에 매핑할 때, 기본 메모리 페이지는 시나리오에 따라 MEM_MAPPED 또는 MEM_IMAGE로 표시됩니다.
MEM_MAPPED/MEM_IMAGE 할당 영역의 단일 바이트 하나라도 수정하면, 시스템은 (수정한 바이트가 PAGE_SIZE 바이트보다 적고 페이지 경계를 넘지 않았다고 가정) 원본 이미지로 다시 매핑되지 않는 조각을 표시하기 위해 단일 메모리 페이지를 분리합니다.
그런 다음 이 관찰은 IOC로 활용됩니다. 이미지는 해당 메모리 영역 내부(그 안에)에 MEM_PRIVATE 할당이 없어야 합니다. 그 이유는 해당 영역 내에서 일부 바이트가 한 번 수정된 적이 있음을 나타내기 때문입니다. Moneta는 비교 시점에 바이트가 원래 모듈의 바이트와 일치하더라도 코드 수정을 올바르게 감지합니다.
Moneta, 프로세스 인젝션 구현 및 관련 IOC가 내부적으로 어떻게 동작하는지에 대한 포괄적인 설명은 Forrest Orr의 다음 최고 수준의 글들을 읽어보시기 바랍니다:
정말 뛰어난 연구와 문서화 작업입니다, Forrest, 훌륭한 작업이에요!
특히 두 번째 글은 이 탐지의 정당성을 설명합니다. Forrest가 가르쳐 주는 내용을 읽어보면:
모듈이 합법적으로 로드되어 PEB에 추가된 경우에도 셸코드 임플란트는 여전히 탐지되었을 것입니다. 주소 공간에 개인 매핑된 0x1000바이트(1페이지) 메모리로 인해, 그리고 워킹 셋을 쿼리하여 Moneta가 이를 검색하기 때문입니다. 그 결과 위에서 본 수정된 코드 IOC가 발생합니다.
요약하자면, 우리는 IOC를 남기고 있지만 그걸 걱정해야 할까요? IOC가 있더라도 훔친 바이트는 보이지 않으므로, 우리 셸코드를 직접 가리키거나 다른 기술과 구분할 수 있는 즉각적인 참조는 없습니다.
요컨대, 그 IOC에 대해 크게 걱정할 필요는 없습니다. :-)
이 구현은 무언가를 남기기 때문에 완벽과는 거리가 멀다고 말할 수 있습니다. 여전히 IOC가 존재하며, 상용 제품들은 그런 특성이 없다는 것을 보여줍니다.
그런 주장이 나왔을 때, 상용 프레임워크는 임플란트와 셸코드 로더의 소스 코드를 완전히 통제할 수 있어 서로를 깔끔하게 통합할 수 있고, 따라서 자체 셸코드를 후킹하거나 우회할 필요가 없다는 점을 상기시킬 필요가 있습니다. 여기서 우리는 Cobalt Strike의 Beacon 실행을 잠들기 직전에 가로채기 위해 kernel32!Sleep을 후킹해야 하며, 그 후에 우리의 하우스키핑을 시작합니다. Sleep을 후킹하지 않고도 개입할 더 나은 메커니즘이 있다면 완벽했을 것입니다.
그러나 Cobalt Strike에는 Sleep Mask 개념이 도입되어 있는데, 크기 제한이 수백 바이트에 불과하여 이 로직을 마스크 자체에 도입할 수 없습니다 (그렇지 않았다면 Sleep도 후킹하지 않아도 되어 상용 제품처럼 IOC를 남기지 않을 수 있었을 것입니다).
또 다른 주장은 상용 프레임워크가 이런 종류의 로직을 _Reflective Loaders_에 통합하는 반면, 우리는 이를 EXE 하네스에 남겨둔다는 점일 수 있습니다. 사실입니다. 하지만 그러한 결정을 내린 이유는 두 가지입니다:
저는 이런 종류의 기술을 공개할 때 매우 신중해야 합니다. 실제 범죄자들이 무기화하는 데 도움을 주어 또 다른 Petya처럼 우리를 괴롭히는 구현이 될 위험을 피해야 하기 때문입니다. 그런 이유로 상용 및 계약 기반 Adversary Simulation 작업에 사용하는 전문 도구에서 사용하는 일부 자세한 내용은 생략하기로 결정했습니다. 씨앗을 내놓는 것이, 적절한 기술을 갖춘 커뮤니티 전문가들이 자신의 도구에서 이 개념을 성장시킬 수 있는 계기가 되기를 바랍니다.
저는 이 전체 로직을 Cobalt Strike의 User-Defined Reflective Loader로 옮기는 것을 훨씬 선호합니다. 이를 통해 Red Team 그룹은 전달 단계에서 더 높은 성공 확률을 얻을 수 있습니다. 하지만 첫째, (1)번 항목을 참고하시고, 둘째, 해당 기술은 현재 RDLL 크기가 5KB로 제한되어 있어 거기에도 이 로직을 구현할 수 없습니다. 사내 Adversary Simulation 작업을 위한 맞춤형 C2 및 임플란트를 구축하는 사람들은 이제 샘플 구현을 받았으므로, 이를 바탕으로 자신의 도구를 더욱 개선하는 데 분명히 도움이 될 것입니다.
코드와 구현을 살펴보고, 개념을 이해한 다음, Red Team 임무를 수행할 때 사용하는 자신만의 Shellcode Loader에 이 개념을 다시 구현하세요. 이것은 고급 인메모리 회피를 위한 또 하나의 기술로, 팀이 Anti-Virus, EDR, 그리고 임플란트를 조사하는 Malware Analysts에게 적발될 확률을 낮춰줍니다.
고급 셸코드 로더를 개발하는 동안 다음 사항도 구현하고 싶을 수 있습니다:
BeaconEye 같은 Beacon 설정 추출기를 회피할 수 있습니다.MEM_PRIVATE 메모리 할당을 찾으려는 스캐너를 회피할 수 있습니다)사용 사례:``` Usage: ShellcodeFluctuation.exe : -1 - Read shellcode but dont inject it. Run in an infinite loop. 0 - Inject the shellcode but don't hook kernel32!Sleep and don't encrypt anything 1 - Inject shellcode and start fluctuating its memory with standard PAGE_READWRITE. 2 - Inject shellcode and start fluctuating its memory with ORCA666's PAGE_NOACCESS.
여기서:
- `<shellcode>`는 셸코드 파일의 경로입니다.
- `<fluctuate>`는 위에서 설명한 대로 `-1`, `0` 또는 `1` 값을 받습니다.
비콘의 스레드 호출 스택을 스푸핑하는 실행 예시:```
C:\> ShellcodeFluctuation.exe ..\..\tests\beacon64.bin 1
[.] Reading shellcode bytes...
[.] Hooking kernel32!Sleep...
[.] Injecting shellcode...
[+] Shellcode is now running. PID = 9456
[+] Fluctuation initialized.
Shellcode resides at 0x000002210C091000 and occupies 176128 bytes. XOR32 key: 0x1e602f0d
[>] Flipped to RW. Encoding...
===> MySleep(5000)
[.] Decoding...
[>] Flipped to RX.
[>] Flipped to RW. Encoding...
===> MySleep(5000)
이 기능을 자체 셸코드 로더/툴링에 추가하려는 경우 반드시 kernel32.dll 언훅(unhook)을 피해야 합니다.
kernel32를 언훅하려고 시도하면 원래 Sleep 기능이 복원되어 콜백이 호출되지 않습니다.
콜백이 호출되지 않으면 스레드는 자체적으로 콜 스택을 스푸핑할 수 없습니다.
그런 동작을 원한다면 별도의 워치독(watchdog) 스레드를 실행하여 Beacons 스레드가 잠들 때마다 스푸핑되도록 해야 할 수도 있습니다.
Cobalt Strike와 Raphael Mudge의 BOF unhook-bof를 사용한다면, 언훅하지 말아야 할 라이브러리를 지정하는 선택적 매개변수를 BOF에 추가하는 Pull Request를 확인해 보세요.
이렇게 하면 kernel32의 훅을 유지할 수 있습니다:``` beacon> unhook kernel32 [*] Running unhook. Will skip these modules: wmp.dll, kernel32.dll [+] host called home, sent: 9475 bytes [+] received output: ntdll.dll <.text> Unhook is done.
[`unhook-bof`를 수정하여 지정된 모듈을 무시하는 옵션 추가](https://github.com/mgeeky/unhook-bof)
---
## 마지막 말
이 PoC는 Cobalt Strike의 Beacon 셸코드와 함께 작동하도록 설계되었습니다. Beacon은 C2의 추가 지시를 기다리기 위해 `kernel32!Sleep`을 호출하는 것으로 알려져 있습니다.
이 로더는 `Sleep`을 후킹하여 자체 정리 작업을 수행함으로써 이 사실을 활용합니다.
이 구현은 `Sleep`을 사용하여 대기하지 않는 다른 셸코드(예: _Meterpreter_)에서는 작동하지 않을 수 있습니다.
이것은 단지 해당 기법을 보여주는 _개념 증명_일 뿐이므로, 다른 C2 프레임워크에 대한 지원을 추가할 의도는 없습니다.
개념을 이해한다면, 분명히 여러분의 셸코드 요구 사항에 맞게 변환하고 솔루션을 여러분에게 유리하도록 적용할 수 있을 것입니다.
"이 코드는 XYZ 셸코드에서 작동하지 않습니다"와 같은 Github 이슈는 열지 말아 주세요. 즉시 닫힙니다.
---
### ☕ 후원하기 ☕
이 프로젝트와 다른 프로젝트들은 잠 못 이루는 밤과 **많은 노력**의 결과입니다. 제가 하는 일을 좋아하고 항상 커뮤니티에 환원하는 것을 감사하게 생각한다면,
[커피 한 잔 사주시면](https://github.com/sponsors/mgeeky) _(또는 맥주가 더 좋습니다)_ 감사하겠습니다! 💪
---
## 저자```
Mariusz Banach / mgeeky, 21
<mb [at] binary-offensive.com>
(https://github.com/mgeeky)