Skip to content
KitploitKITPLOIT
도구익스플로잇블로그
Log in
제출
도구익스플로잇블로그
제출

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

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

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

도구 디렉토리

카테고리

모든 카테고리 보기
Loading categories
ThreadStackSpoofer — Thread Stack Spoofing - 고급 인메모리 우회 기법의 PoC로, 주입된 셸코드의 메모리 할당을 스캐너와 분석가로부터 더 잘 숨길 수 있도록 합니다. | Kitploit
도구/GitHubGitHub/mgeeky/threadstackspoofer
Penetration Testing FrameworksExploit FrameworksMemory ForensicsShellcodeMalware AnalysisRed TeamingPayload Development
GitHubmgeeky/threadstackspoofer

ThreadStackSpoofer

Thread Stack Spoofing - 고급 인메모리 우회 기법의 PoC로, 주입된 셸코드의 메모리 할당을 스캐너와 분석가로부터 더 잘 숨길 수 있도록 합니다.

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

인기

모두 보기 →

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

모든 도구 탐색

도구 컬렉션을 둘러보세요

모든 도구 보기 →
공유
# Thread Stack Spoofing / Call Stack Spoofing PoC

스레드 호출 스택을 스푸핑하는 고급 인메모리 회피 기법의 PoC 구현입니다. 이 기술은 스레드 기반 메모리 검사 규칙을 우회하고 인-프로세스 메모리에서 셸코드를 더 잘 숨길 수 있게 해줍니다.

## 소개

이는 검사 중인 스레드의 호출 스택에서 셸코드 프레임에 대한 참조를 찾는 악성코드 분석가, AV 및 EDR을 회피하기 위한 _Thread Stack Spoofing_ 기법의 예제 구현입니다. 아이디어는 스레드의 호출 스택에서 셸코드에 대한 참조를 숨겨 악성코드 코드가 포함된 할당을 위장하는 것입니다.

이 구현은 제 [ShellcodeFluctuation](https://github.com/mgeeky/ShellcodeFluctuation)과 함께 상용 C2 제품이 제공하는 기능을 따라잡기 위한 모범 구현을 공격적 보안 커뮤니티에 제공하여, Red Team 도구에서 뒤처지지 않도록 합니다. 💪

### 구현 변경 사항

현재 구현은 원래 공개된 것과 크게 다릅니다. 그 이유는 우리가 제어하는 첫 번째 프레임의 반환 주소에 `0`을 쓰기만 하면 스레드 호출 스택 처리를 종료하고 셸코드 관련 프레임을 숨기는 훨씬 간단한 방법이 있다는 것을 깨달았기 때문입니다:

```
void WINAPI MySleep(DWORD _dwMilliseconds)
{
    [...]
    auto overwrite = (PULONG_PTR)_AddressOfReturnAddress();
    const auto origReturnAddress = *overwrite;
    *overwrite = 0;

    [...]
    *overwrite = origReturnAddress;
}
```

`StackWalk64`를 활용한 이전 구현은 이 [커밋 c250724](https://github.com/mgeeky/ThreadStackSpoofer/tree/c2507248723d167fb2feddf50d35435a17fd61a2)에서 확인할 수 있습니다.

이 구현은 훨씬 안정적이며 두 아키텍처(`x64` 및 `x86`)에서 `Debug` 및 `Release` 모두에서 잘 작동합니다.

## 데모

호출 스택이 **스푸핑되지 않은** 경우의 모습입니다:

![not-spoofed](https://assets.kitploit.com/production/public/readmes/4783/3b8962b3574320c202b79b901eedc739455970dc7d01c94d013012374cb58cac.png)

반면, 스레드 스택 스푸핑이 활성화된 경우입니다:

![spoofed](https://assets.kitploit.com/production/public/readmes/4783/8b3bc4e864e2087abb351adeec43edd8d59b0de6024302ac911befbabdb87407.png)

위에서 볼 수 있듯이 호출 스택의 마지막 프레임은 `MySleep` 콜백입니다. 이것이 즉시 새로운 IOC 기회를 제공하는지 궁금할 수 있습니다. 헌팅 규칙은 호출 스택이 시스템 라이브러리 내에 있는 다음과 같은 예상 스레드 진입점으로 풀리지 않는 스레드를 찾을 수 있습니다:

```
kernel32!BaseThreadInitThunk+0x14
ntdll!RtlUserThreadStart+0x21
```

그러나 스푸핑된 스레드의 호출 스택은 처음에는 다소 이상해 보일 수 있습니다. 제 시스템을 간략히 살펴보면 위의 진입점으로 풀리지 않는 다른 스레드도 있음을 보여줍니다:

![legit call stack](https://assets.kitploit.com/production/public/readmes/4783/62193f641b5e4f052a6a2700d50f821bb9580e4213c6a04a40f77174006e004d.png)

위 스크린샷은 수정되지 않은 **Total Commander x64**의 스레드를 보여줍니다. 보시다시피, 초기 호출 스택 프레임 측면에서 우리의 것과 매우 유사합니다.

단순히 모방할 수 있는 특성을 보이는 프로세스가 있는데 왜 호출 스택을 신중하게 위조해야 할까요?

## 작동 방식

대략적인 알고리즘은 다음과 같습니다:

1. 파일에서 셸코드 내용을 읽습니다.
2. `dbghelp.dll`에서 필요한 모든 함수 포인터를 획득하고 `SymInitialize`를 호출합니다.
3. `kernel32!Sleep`을 후킹하여 우리의 콜백을 가리키게 합니다.
4. `VirtualAlloc` + `memcpy` + `CreateThread`를 통해 셸코드를 인젝션하고 실행합니다. 스레드는 우리의 `runShellcode` 함수에서 시작해야 하며, 스레드의 _StartAddress_가 예상치 못한 비정상적인 곳(예: `ntdll!RtlUserThreadStart+0x21`)을 가리키는 것을 방지합니다.
5. Beacon이 Sleep을 시도하는 즉시 우리의 `MySleep` 콜백이 호출됩니다.
6. 그런 다음 스택의 마지막 반환 주소를 `0`으로 덮어쓰며, 이는 효과적으로 호출 스택을 종료합니다.
7. 마지막으로 `::SleepEx`를 호출하여 추가 통신을 기다리는 동안 Beacon의 Sleep을 허용합니다.
8. Sleep이 완료된 후, 이전에 저장된 원래 함수 반환 주소를 복원하고 실행이 재개됩니다.

함수 반환 주소는 `RBP/EBP` 레지스터가 가리키는 스레드 스택 메모리 영역 전체에 흩어져 있습니다. 스택에서 이를 찾으려면 먼저 프레임 포인터를 수집한 다음 역참조하여 덮어써야 합니다:

![stack frame](https://assets.kitploit.com/production/public/readmes/4783/624e562610d8dfc6d15dd84cab8e2a89015e25f37f5c4a1f57b955c37ef4ffbd.png)

_(위 이미지는 **Eli Bendersky**의 게시물 [Stack frame layout on x86-64](https://eli.thegreenplace.net/2011/09/06/stack-frame-layout-on-x86-64/)에서 가져왔습니다.)_

```
	*(PULONG_PTR)(frameAddr + sizeof(void*)) = Fake_Return_Address;
```

`ThreadStackSpoofer`의 초기 구현은 `walkCallStack` 및 `spoofCallStack` 함수에서 그렇게 했지만, 현재 구현은 이러한 노력이 _은밀한 호출 스택 유지에 필요하지 않음_을 보여줍니다.

## 실행 예시

사용법:

```
C:\> ThreadStackSpoofer.exe <shellcode> <spoof>
```

여기서:
- `<shellcode>`는 셸코드 파일의 경로입니다.
- `<spoof>`가 `1` 또는 `true`이면 스레드 스택 스푸핑을 활성화하고, 그 외의 값이면 비활성화합니다.

비콘의 스레드 호출 스택을 스푸핑하는 실행 예시:

```
PS D:\dev2\ThreadStackSpoofer> .\x64\Release\ThreadStackSpoofer .\tests\beacon64.bin 1
[.] Reading shellcode bytes...
[.] Hooking kernel32!Sleep...
[.] Injecting shellcode...
[+] Shellcode is now running.
[>] Original return address: 0x1926747bd51. Finishing call stack...

===> MySleep(5000)

[<] Restoring original return address...
[>] Original return address: 0x1926747bd51. Finishing call stack...

===> MySleep(5000)

[<] Restoring original return address...
[>] Original return address: 0x1926747bd51. Finishing call stack...
```

---

## 어떻게 사용하나요?

코드와 구현을 살펴보고 개념을 이해한 후 Red Team 작업을 전달하는 데 사용하는 자체 셸코드 로더에 개념을 재구현하세요. 이는 고급 인메모리 회피를 위한 또 다른 기술로, 팀이 안티바이러스, EDR 및 임플란트를 검사하는 악성코드 분석가에게 적발되지 않을 가능성을 높여줍니다.

고급 셸코드 로더를 개발하는 동안 다음을 구현하는 것도 고려할 수 있습니다:

- **프로세스 힙 암호화** - 이 블로그 게시물에서 영감을 얻으세요: [Hook Heaps and Live Free](https://www.arashparsa.com/hook-heaps-and-live-free/) - [`BeaconEye`](https://github.com/CCob/BeaconEye)와 같은 Beacon 구성 추출기를 회피할 수 있습니다.
- **Beacon의 메모리 페이지 보호를 `RW`로 변경하고 내용을 암호화** - [Shellcode Fluctuation](https://github.com/mgeeky/ShellcodeFluctuation) 기법 사용 - Sleep 직전에 수행 ( [`Moneta`](https://github.com/forrest-orr/moneta) 또는 [`pe-sieve`](https://github.com/hasherezade/pe-sieve)와 같은 스캐너를 회피할 수 있습니다.)
- **Reflective Loader의 잔여물을 모두 제거**하여 인메모리 시그니처 기반 탐지를 피합니다.
- **후킹한 모든 것을 언후킹** (AMSI, ETW, WLDP 등)하고 Sleep 후 다시 후킹합니다.

---

## 사실 이것은 (아직) 진정한 스택 스푸핑이 아닙니다

지적된 바와 같이, 여기서의 기술은 _스택 스푸퍼_라는 이름에 아직 진정으로 부합하지 않습니다. 단순히 스레드 스택의 반환 주소를 덮어쓰기 때문에 스택 자체의 나머지 영역을 스푸핑하는 것은 아닙니다. 게다가 호출 스택을 _언와인더블(unwindable)_하게 남겨두어 시스템이 전체 호출 스택 프레임 체인을 제대로 탐색할 수 없기 때문에 이상하게 보입니다.

하지만 이러한 단점을 알고 있지만, 현재로서는 그대로 두었습니다. 주로 프로세스를 반복하고, 스레드를 열거하고, 스레드 스택을 탐색하며 비이미지 메모리(예: `SEC_PRIVATE` - `VirtualAlloc` 등에 의해 동적으로 할당된 메모리)를 가리키는 반환 주소를 찾는 자동화된 스캐너를 회피하는 데 주로 관심이 있었기 때문입니다. 집중된 악성코드 분석가는 즉시 이상함을 발견하고 스레드를 상당히 비정상적으로 간주하여 임플란트를 추적할 것입니다. 확신합니다. 그러나 현재 AV/EDR과 같은 자동화된 스캐너가 각 스레드의 스택을 _실제로 탐색_하여 언와인더블(un-windable) 여부를 확인하는 휴리스틱을 구현했다고 생각하지 않습니다 `¯\_(ツ)_/¯`.

확실히 이 프로젝트(및 C2 프레임워크에서 발견되는 상용 구현)는 AV 및 EDR 공급업체가 이러한 새로운 회피 기술을 다루는 적절한 휴리스틱 구현을 고려해야 하는 근거를 제공합니다.

이 기술을 개선하기 위해 역언와인딩 프로세스에서 신중하게 제작된 가짜 스택 프레임을 삽입하여 진정한 _Thread Stack Spoofer_를 목표로 할 수 있습니다. 
아래에서 이 아이디어에 대해 자세히 읽어보세요.

### 진정한 Thread Stack Spoofer 구현

[namazso](https://twitter.com/namazso)와의 수 시간에 걸친 대화를 통해 적절한 스레드 스택 스푸퍼를 목표로 하려면 x64 호출 스택 언와인딩 프로세스를 역공학해야 한다는 것을 배웠습니다. 
먼저 아래 (a) 링크에서 설명된 스택 언와인딩 프로세스를 주의 깊게 이해해야 합니다. 시스템이 x64 아키텍처에서 스레드 호출 스택을 탐색할 때 단순히 스레드 스택에 흩어져 있는 반환 주소에 의존하지 않고 다음과 같이 수행합니다:

1. 반환 주소를 가져옵니다.
2. 해당 주소를 포함하는 함수를 식별하려고 시도합니다([RtlLookupFunctionEntry](https://docs.microsoft.com/en-us/windows/win32/api/winnt/nf-winnt-rtllookupfunctionentry) 사용).
3. 해당 함수는 `RUNTIME_FUNCTION`, `UNWIND_INFO` 및 `UNWIND_CODE` 구조체를 반환합니다. 이러한 구조체는 함수의 시작 주소, 종료 주소 및 `RBP` 또는 `RSP`를 수정하는 모든 코드 시퀀스의 위치를 설명합니다.
4. 시스템은 호출 스택 전체의 각 함수에서 발생한 모든 스택 및 프레임 포인터 수정 사항을 알아야 하며, 그런 다음 이러한 변경 사항을 가상으로 _롤백_하고 처리된 호출 스택 프레임에 대한 호출이 발생했을 때 호출 스택 포인터를 가상으로 복원해야 합니다([RtlVirtualUnwind](https://docs.microsoft.com/ru-ru/windows/win32/api/winnt/nf-winnt-rtlvirtualunwind)에서 구현됨).
5. 시스템은 검사된 함수가 보여주는 모든 `UNWIND_CODE`를 처리하여 해당 프레임의 반환 주소 및 스택 포인터 값의 위치를 정확히 계산합니다.
6. 이 에뮬레이션을 통해 시스템은 호출 스택 체인을 따라 내려가며 호출 스택을 효과적으로 "언와인드(unwind)"할 수 있습니다.

이 프로세스를 방해하려면 `RtlVirtualUnwind`의 역(reverted) 형태를 만들어 _되돌려야_ 합니다. 모듈(예: `kernel32`)에 정의된 함수를 반복하고, 각 함수의 `UNWIND_CODE` 코드를 스캔하고, 이를 역방향으로 면밀히 에뮬레이션하여(`RtlVirtualUnwind` 및 정확히 `RtlpUnwindPrologue`와 비교) 가짜 반환 주소를 배치할 스택의 위치를 찾아야 합니다.

[namazso](https://twitter.com/namazso)는 호출 스택을 깔끔하게 연결하기 위해 3개의 가짜 스택 프레임을 도입해야 한다고 언급합니다:

1. `MySleep`의 호출자와 다르게 언와인드되는 "desync" 프레임(가젯 프레임(_gadget-frame_)으로 간주)입니다(다른 `UWOP` - Unwind Operation 코드 사용). 모듈의 모든 함수를 살펴보고 UWOP를 확인하여 가짜 프레임의 크기를 계산합니다. 이 프레임은 `MySleep`의 호출자와 **다른** UWOP를 가져야 합니다.
2. 다음으로 찾고자 하는 프레임은 스택에서 `RBP`로 팝(pop)하여 언와인드되는 함수입니다. 기본적으로 `UWOP_PUSH_NONVOL` 코드를 통해 수행됩니다.
3. 세 번째 프레임은 `UWOP_SET_FPREG` 코드를 통해 `RBP`에서 `RSP`를 복원하는 함수가 필요합니다.

복원된 `RSP`는 제어 흐름이 `MySleep`으로 진입한 지점에서 가져온 `RSP`로 설정되어야 하며, 세 번째 가젯이 거기서 언와인드됨에 따라 우리의 모든 프레임이 숨겨집니다.

프로세스를 시작하려면 `IMAGE_DIRECTORY_ENTRY_EXCEPTION` 데이터 디렉터리 항목을 역참조하여 실행 파일의 `.pdata`를 반복할 수 있습니다. 다음 예를 고려하세요:

```
    ULONG_PTR imageBase = (ULONG_PTR)GetModuleHandleA("kernel32");
    PIMAGE_NT_HEADERS64 pNthdrs = PIMAGE_NT_HEADERS64(imageBase + PIMAGE_DOS_HEADER(imageBase)->e_lfanew);

    auto excdir = pNthdrs->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXCEPTION];
    if (excdir.Size == 0 || excdir.VirtualAddress == 0)
        return;

    auto begin = PRUNTIME_FUNCTION(excdir.VirtualAddress + imageBase);
    auto end = PRUNTIME_FUNCTION(excdir.VirtualAddress + imageBase + excdir.Size);

    UNWIND_HISTORY_TABLE mshist = { 0 };
    DWORD64 imageBase2 = 0;

    PRUNTIME_FUNCTION currFrame = RtlLookupFunctionEntry(
        (DWORD64)caller,
        &imageBase2,
        &mshist
    );

    UNWIND_INFO *mySleep = (UNWIND_INFO*)(currFrame->UnwindData + imageBase);
    UNWIND_CODE myFrameUwop = (UNWIND_CODE)(mySleep->UnwindCodes[0]);

    log("1. MySleep RIP UWOP: ", myFrameUwop.UnwindOpcode);

    for (PRUNTIME_FUNCTION it = begin; it < end; ++it)
    {
        UNWIND_INFO* unwindData = (UNWIND_INFO*)(it->UnwindData + imageBase);
        UNWIND_CODE frameUwop = (UNWIND_CODE)(unwindData->UnwindCodes[0]);

        if (frameUwop.UnwindOpcode != myFrameUwop.UnwindOpcode)
        {
            // Found candidate function for a desynch gadget frame

        }
    }
```

이 과정은 다소 복잡하지만, ROP와 유사한 방식으로 임의의 스택 프레임을 신중하게 선택된 다른 프레임으로 대체하여 스레드 호출 스택 언와인딩 프로세스를 되돌리는 것으로 귀결됩니다.

이 PoC는 이 알고리즘을 복제하지 않습니다. 현재 이해로는 호출 스택이 `EXE` 기반 스택 프레임에서 끝나는 것을 받아들일 수 있고, 셸코드 로더나 이 PoC를 지나치게 복잡하게 만들고 싶지 않기 때문입니다. 이를 구현하고 공개적으로 공유하는 것은 열성적인 독자에게 맡깁니다. 또는 시간이 더 있으면 직접 시도해 볼 수도 있습니다 :)

**추가 정보**:

- **a)** [x64 예외 처리 - 스택 언와인딩 프로세스 설명](https://docs.microsoft.com/en-us/cpp/build/exception-handling-x64?view=msvc-160)
- **b)** [`RtlpUnwindPrologue` 및 `RtlVirtualUnwind` 샘플 구현](https://github.com/mic101/windows/blob/master/WRK-v1.2/base/ntos/rtl/amd64/exdsptch.c)
- **c)** [`.pdata` 섹션](https://docs.microsoft.com/en-us/windows/win32/debug/pe-format#the-pdata-section)
- **d)** [`RtlpUnwindPrologue`의 또 다른 샘플 구현](https://github.com/hzqst/unicorn_pe/blob/master/unicorn_pe/except.cpp#L773)

---

## 주의사항

자체 셸코드 로더/도구에 이 기능을 추가할 계획이라면 `kernel32.dll`을 언후킹하지 **않도록 주의**하세요. `kernel32`을 언후킹하면 원래 `Sleep` 기능이 복원되어 콜백이 호출되지 않습니다. 콜백이 호출되지 않으면 스레드가 자체적으로 호출 스택을 스푸핑할 수 없습니다.

그런 것이 필요하다면, 다른 감시(watchdog) 스레드를 실행하여 Beacon 스레드가 Sleep할 때마다 스푸핑되도록 해야 할 수도 있습니다.

Cobalt Strike와 Raphael Mudge의 BOF `unhook-bof`를 사용하는 경우, 언후킹하지 않아야 할 라이브러리를 지정하는 선택적 매개변수를 추가한 [Pull Request](https://github.com/Cobalt-Strike/unhook-bof/pull/1)를 확인하세요. 이렇게 하면 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_)에서는 작동하지 않을 수 있습니다. 이는 단지 기술을 보여주는 _Proof of Concept_일 뿐이므로, 다른 C2 프레임워크에 대한 지원을 추가할 의도는 없습니다.

개념을 이해하면, 셸코드 요구 사항에 맞게 변환하고 유리하게 솔루션을 적용할 수 있을 것입니다.

"이 코드는 XYZ 셸코드에서 작동하지 않습니다"와 관련된 Github 이슈를 열지 마십시오. 즉시 종료됩니다.

---

### ☕ 지원하기 ☕

이 프로젝트와 다른 프로젝트들은 잠 못 이루는 밤과 **많은 노력**의 결과입니다. 제가 하는 일이 마음에 들고 항상 커뮤니티에 환원하는 것을 감사하게 생각한다면, [커피 한 잔 사주시는 것](https://github.com/sponsors/mgeeky)을 고려해 주세요 _(또는 맥주가 더 좋습니다)_ 감사 인사를 전하는 의미로! 💪

---

## 저자

```   
   Mariusz Banach / mgeeky, 21
   <mb [at] binary-offensive.com>
   (https://github.com/mgeeky)
```
도구 다운로드