Skip to content
KitploitKITPLOIT
도구블로그
제출
도구블로그
제출

해킹, 침투 테스트 및 사이버 보안 도구를 당신의 보안 무기고에!

Kitploit은 해킹, 사이버 보안 및 침투 테스트 도구 디렉토리입니다. 최신 프로젝트 업데이트를 발견하여 취약점을 찾고, 시스템을 분석하고, 테스트를 자동화하고, 보안을 강화하세요.

··피드·문의·개인정보·© 2026 Kitploit

도구 디렉토리

카테고리

모든 카테고리 보기
Loading categories
도구/GitHubGitHub/lem0nsec/shellghost
Encryption/Decryption ToolsMemory ForensicsExploitationShellcodeBinary AnalysisRed TeamingPayload Development
GitHublem0nsec/shellghost

ShellGhost

프로세스 시작부터 종료까지 셸코드를 보이지 않게 하는 메모리 기반 우회 기술.

저장소 보기
1.2k1402년 전Kitploit 검토 완료

인기

모두 보기 →

커뮤니티에서 가장 많이 사용되는 도구를 찾아보세요.

모든 도구 탐색

도구 컬렉션을 둘러보세요

모든 도구 보기 →
공유

ShellGhost

프로세스 시작부터 종료까지 쉘코드를 보이지 않게 만드는 메모리 기반 회피 기법입니다.


동기

AV/EDR 회피 개념 중 일부를 보여주는 이 쉘코드 셀프 인젝션 POC를 공유하고자 합니다. 이 개념들은 레드 팀 운영에 유용할 수 있습니다. 불과 몇 주 전에 저는 ShellGhost라고 명명한 커스텀 인메모리 회피 기법을 떠올렸습니다. 이 기법은 __프로세스 시작부터 끝까지 '보이지 않는' 쉘코드를 실행하는 코드__에 대한 필요성에서 비롯되었습니다.


스레드 실행 흐름 처리

ShellGhost는 벡터 예외 처리와 소프트웨어 중단점을 결합하여 주기적으로 스레드 실행을 중단하고, 실행된 중단점을 RC4 암호화된 쉘코드 명령어로 대체한 후 명령어를 복호화하고 메모리 보호를 RX로 복원한 후 실행을 재개합니다. 이후 EXCEPTION_BREAKPOINT가 발생하면 예외 처리기가 이전 쉘코드 명령어를 새 중단점으로 대체하여 할당 메모리 내에서 완전한 쉘코드가 암호화되지 않은 상태로 노출되는 것을 방지합니다. 이 과정은 처음에 READ/WRITE로 표시된 프라이빗 메모리 페이지 내에서 이루어집니다. RW PRV 할당이 있다는 것은 PE-Sieve나 Moneta와 같은 메모리 스캐너에 의해 '침해 지표(IoC)'로 간주되지 않습니다. 할당이 RX가 되고 페이지가 스캔될 때는 중단점 외에는 아무것도 발견되지 않습니다. 이는 쉘코드가 실제로 실행 중인 동안에도 발생합니다. 다음 그림은 리버스 셸이 실행 중이지만 Moneta가 어떤 IoC도 발견하지 못함을 보여줍니다(바이너리가 서명되지 않은 점 제외).

Pe-Sieve로 프로세스를 스캔하려 하면 더 나은 결과를 얻을 수 있습니다:


쉘코드 매핑

쉘코드 매핑은 ShellGhost의 핵심 기능입니다. 이 전략을 통해 스레드는 명령어를 간헐적으로 실행하면서도 메모리에 전체 쉘코드를 노출하지 않을 수 있습니다. 이는 스레드가 실행하는 각각의 쉘코드 명령어 위치가 할당된 메모리 페이지 내에 있는 특정 중단점의 위치와 일치하기 때문에 가능합니다. ShellGhost는 스레드 RIP에서 할당된 메모리 페이지의 기본 주소까지의 상대 가상 주소(RVA)를 계산하고, 이를 암호화된 쉘코드/암호화된 명령어의 기본 주소에 더하여 이 위치를 확인합니다. 교체되는 중단점의 개수는 항상 동일하지 않으며, 각 명령어가 올바르게 생성되고 해석되기 위해 필요한 오퍼코드의 개수(QUOTA)에 따라 달라집니다. 예를 들어 'POP RBP' 명령어는 '5D'에 해당하므로 하나의 중단점만 교체됩니다. 반면 'JMP RAX' 명령어는 'FF E0' 오퍼코드가 필요하므로 두 개의 중단점이 교체됩니다. 이러한 이유로 다음과 같은 C 데이터 구조를 만들었습니다.

root@kitploit:~
typedef struct CRYPT_BYTES_QUOTA {

	DWORD RVA;		// 암호화된 명령어까지의 오프셋 
	DWORD quota;	// 명령어를 생성하는 오퍼코드의 개수

} CRYPT_BYTES_QUOTA, * PCRYPT_BYTES_QUOTA;

중단점은 즉시 해당 명령어로 교체되지 않습니다. 명령어가 실행되기 전에 복호화 과정을 거쳐야 하기 때문입니다. 여기서 DWORD quota가 사용됩니다. ShellGhost는 현재 널리 사용되는 'SystemFunction032'를 이용해 RC4 복호화를 수행합니다. XOR과 달리 RC4는 단일 바이트 암호화 방식이 아닙니다. 즉, 쉘코드를 한 번에 모두 암호화하고 복호화할 수 없습니다. 이것이 각 명령어를 개별적으로 처리해야 하는 또 다른 이유입니다. 중단점이 교체된 후, SystemFunction032가 필요로 하는 버퍼 길이는 '명령어 quota'와 동일하며, 이는 특정 명령어를 구성하는 오퍼코드 개수를 나타냅니다. 예를 들어, 다음 코드 조각을 고려해 보세요.

root@kitploit:~

CRYPT_BYTES_QUOTA instruction[200];
instruction[5].quota = 2

USTRING buf = { 0 }; 	// 복호화할 버퍼와 그 길이를 포함
USTRING key = { 0 }; 	// RC4 키와 길이를 포함

buf.Length = 2 		// 버퍼 길이, 또는 복호화할 명령어의 길이

쉘코드 명령어 5번은 2개의 오퍼코드로 구성되므로, SystemFunction032에 버퍼 길이 2가 전달됩니다. 이는 SystemFunction032를 한 번 호출하여 전체 쉘코드를 복호화하려고 하면 전체가 손상되기 때문에 중요합니다.

쉘코드 매핑은 어떻게 수행되나요?

컴파일 전에 ShellGhost_mapping.py로 쉘코드를 매핑해야 합니다. 이 스크립트는 각 명령어를 추출하여 작고 독립적인 쉘코드로 취급합니다. 명령어는 하나씩 암호화된 후 C 형식으로 출력되어 unsigned char로 모두 함께 나타납니다. 그 결과는 C 코드 내에 하드코딩될 수 있습니다. 아래는 calc.exe용 암호화된 MSF 쉘코드 명령어의 예입니다.

이 쉘코드는 98개의 명령어를 가지므로 98개의 CRYPT_BYTES_QUOTA 구조체가 선언됩니다. 코드가 실행될 때 이 구조체들은 적절한 명령어 RVA와 QUOTA로 채워져야 합니다. '-1' 매개변수는 매핑 스크립트가 이를 수행하는 코드 조각을 출력하도록 지시합니다.

Winapi 매개변수 조정

Metasploit x64 쉘코드는 일반적으로 명령어 사이에 winapi 문자열 매개변수를 저장합니다. 즉, MSF x64 쉘코드가 Winexec를 호출할 때 스택에 첫 번째 매개변수 문자열을 갖기 위해 nullbyte로 끝나는 일련의 바이트를 푸시하지 않습니다. 대신, RCX 레지스터(첫 번째 매개변수)는 다음 그림과 같이 쉘코드 자체 내부의 포인터입니다.

이는 문자열과 관련된 위치의 중단점이 절대 해결되지 않음을 의미합니다. RIP가 그 위치를 절대 건드리지 않기 때문입니다. 실제로 이 코드는 RIP가 통과하는 실제 쉘코드 명령어를 해결하는 것이지, 명령어처럼 실행되지 않는 매개변수를 해결하는 것이 아닙니다. 이 문제를 해결하기 위해, MSF 쉘코드가 항상 호출하는 winapi에 대한 포인터를 RAX 레지스터에 저장한 후 레지스터 자체로 점프한다는 점을 발견했습니다. 따라서 ShellGhost VEH는 해결된 중단점이 'JMP RAX'이고 RCX 레지스터가 쉘코드 내부의 위치를 가리키는 경우, RCX가 가리키는 위치도 해결하려고 시도합니다. 그 후 실행은 할당된 메모리로 반환되지 않습니다. 대신, RAX(winapi 주소)가 RIP에 복사되고 스레드 실행이 winapi에서 재개되어 'JMP RAX'를 무효화하고 할당된 메모리를 RW로 유지합니다. 이는 WaitForSingleObject를 호출하는 리버스 셸에 필요합니다. 그렇지 않으면 'JMP RAX' 후 스레드가 대기 상태가 되어 셸이 살아있는 동안 메모리가 RX로 유지되기 때문입니다. 다음 코드 조각에는 RCX 레지스터가 winapi 매개변수 문자열을 포함할 때 ShellGhost가 이를 조정하고 MSF 쉘코드가 함수 호출(여기서는 WinExec)을 올바르게 수행할 수 있도록 하는 두 가지 조건이 포함되어 있습니다.

root@kitploit:~
<snip>
	
if (*(PWORD)exceptionData->ContextRecord->Rip == 0xe0ff) // RIP가 'JMP RAX'인 경우

<snip>

if ((contextRecord->Rcx >= (DWORD_PTR)allocation_base) && (contextRecord->Rcx <= ((DWORD_PTR)allocation_base + sizeof(sh)))) // RCX가 할당 내부에 있는 경우

<snip>

RDX, R8, R9(두 번째, 세 번째, 네 번째 매개변수)는 아직 다루지 않았습니다.

다른 기술들과의 차이점 및 유사점

ShellcodeFluctuation은 매우 유사한 인메모리 회피 개념입니다. 여기서도 할당된 메모리는 RW에서 RX로 '변동'합니다. 반면, ShellGhost는 다음과 같은 개선 사항을 도입합니다.

  • 단일 바이트 XOR 대신 RC4 암호화 및 '쉘코드 매핑'
  • 함수 후킹 불필요
  • Metasploit 쉘코드 지원

하지만 ShellGhost는 완벽한 기법과는 거리가 있습니다. 여전히 이러한 모든 기법이 가지는 가장 큰 단점, 즉 __실행 중 어느 시점에서는 프라이빗 실행 가능 메모리가 필요하다는 점__을 안고 있습니다. Foliage와 같은 더 발전된 기법은 이미 이 문제를 우회하는 방법을 찾았습니다. 또한, 소프트웨어 중단점으로 가득 찬 메모리 할당은 YARA 규칙에 의해 탐지될 수 있습니다. 다음 그림은 Moneta가 RX PRV 할당에 대한 IoC를 올바르게 탐지하는 모습을 보여줍니다.

EDR 솔루션을 회피하는 데 있어 메모리 스캐닝은 더 큰 그림의 일부일 뿐입니다. IoC가 완전히 없다고 해서 이 기법을 사용하는 바이너리가 특정 EDR에 효과적일 것이라는 의미는 아닙니다. 제가 알기로는, 솔루션이 사용자가 시도하는 방식으로 바이너리를 실행하는 것조차 허용하지 않는 상황을 경험했습니다. 반면에, IoC가 항상 정확한 지표는 아니며 일부는 오탐(false positive)으로 판명될 수도 있습니다. 따라서 이것은 독자가 이해해 주길 바라는 단순한 원시 기법이자 영감입니다. 레드 팀원은 EDR의 구성 요소와 마찬가지로 인메모리 회피도 엔진의 하나의 구성 요소에 불과하다는 것을 알고 있습니다.

참고 사항

컴파일하려면 증분 링크를 비활성화해야 합니다. 이 VS 프로젝트에는 모든 컴파일러/링커 옵션이 이미 설정되어 있습니다.

도구 다운로드