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

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

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

피드문의개인정보© 2026 Kitploit

도구 디렉토리

카테고리

모든 카테고리 보기
Loading categories
lacuna-rs — Windows x64용 ghost-frame call-stack spoofing, runtime indirect syscalls 및 APC injection을 위한 Rust crate입니다. SSN resolution, JIT stub emission 및 VEH-based parameter encryption을 제공하여 EDR evasion을 지원합니다. | Kitploit
도구/GitHubGitHub/karkas66/lacuna-rs
ShellcodePost-ExploitationRed TeamingPayload DevelopmentBinary Exploitation
GitHubkarkas66/lacuna-rs

lacuna-rs

Windows x64용 ghost-frame call-stack spoofing, runtime indirect syscalls 및 APC injection을 위한 Rust crate입니다. SSN resolution, JIT stub emission 및 VEH-based parameter encryption을 제공하여 EDR evasion을 지원합니다.

저장소 보기
1817218일 전Kitploit 검토 완료

인기

모두 보기 →

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

모든 도구 탐색

도구 컬렉션을 둘러보세요

모든 도구 보기 →
공유

lacuna-rs

Windows x64용 고스트 프레임 호출 스택 스푸핑 + 런타임 간접 시스템 콜 — Rust로 포팅됨.

Mohamed Alzhrani(0xmaz)의 LACUNA Chain(lacuna_chain.c)에서 포팅됨.

lacuna-rs는 원본 C TTP와 동일한 프리미티브를 제공하는 재사용 가능한 Rust 크레이트로, wsyscall-rs나 syscalls-rs처럼 모든 Rust 프로젝트에 바로 적용할 수 있도록 구조화되어 있지만, 런타임 SSN 해석, 함수별 syscall;ret 타겟팅, 그리고 고스트 프레임 스택 스푸핑을 갖추고 있습니다.


중요: 프레임 포인터 요구사항

stack-spoof 기능은 이를 사용하는 크레이트가 프레임 포인터와 함께 컴파일되지 않으면 조용히 실패합니다.

chain.rs의 스택 스톰핑 프리미티브는 mov rbp, {x} 인라인 어셈을 통해 호출자의 반환 주소 슬롯을 찾습니다. 이를 위해서는 RBP 체인이 온전해야 합니다. Rust(그리고 대부분의 릴리스 모드 컴파일러)는 기본적으로 프레임 포인터를 생략합니다.

build.rs는 이 크레이트 자체의 코드 생성에 대해 force-frame-pointers=yes를 설정하지만, Cargo는 컴파일러 플래그를 하위 크레이트로 전파할 수 없습니다. 다음을 자신의 프로젝트에 추가해야 합니다:```toml

.cargo/config.toml (in YOUR crate, not in lacuna-rs)

[build] rustflags = ["-C", "force-frame-pointers=yes"]

이것이 없으면 `stomp_plant()`는 RBP에서 가비지 값을 읽어들이고, 최선의 경우 아무 동작도 하지 않거나 최악의 경우 스택을 손상시킵니다. 이 크레이트는 런타임에 프레임 포인터가 활성화되어 있는지 감지할 방법이 없으며, 단순히 작동하지 않을 것입니다.

스캐닝, SSN 해석 또는 인젝션 프리미티브만 필요하다면(스택 스푸핑 없이), `stack-spoof` 기능을 생략할 수 있으며 이 요구 사항은 적용되지 않습니다.

---

## 수행하는 작업

| 프리미티브 | C 함수 | Rust 모듈 |
|---|---|---|
| PE 섹션 + 익스포트 파싱 | `pe_section()`, `pe_export()` | `pe` |
| `.pdata` 고스트 영역 스캐닝 | `scan_ghosts()`, `best_ghost()` | `scan` |
| 고스트 가젯 탐색 (`jmp [rbx]`) | `scan_ghost_gadgets()` | `scan` |
| `win32u` NOP 갭 탐색기 | `win32u_nop_gap()` | `scan` |
| BYOUD-MF 앵커 탐색기 | `find_mf_target()` | `scan` |
| SSN 해석 (Hell's Gate / Halo's Gate) | `resolve_ssn()` | `nt` |
| 함수별 `syscall;ret` 로케이터 | `find_func_syscall()` | `nt` |
| JIT 간접 시스템 콜 스텁 생성 | `alloc_stub()` | `stub` |
| 고스트 가젯 스텁 리다이렉트 | (`alloc_stub()` 내) | `stub` |
| VEH + 하드웨어 브레이크포인트 파라미터 암호화 | `param_encrypt_veh()`, `pcrypt_arm()` | `veh` |
| 체인 가드 VEH | `chain_veh()` | `veh` |
| LACUNA 체인 구성 | `build_chain()` | `chain` |
| 스택 스톰프 (BYOUD-RT) | `stomp_plant()`, `stomp_restore()` | `chain` |
| 체인 워커 (검증) | `lacuna_walk_chain()` | `chain` |
| 섹션 기반 APC 인젝션 | `do_inject_sapc()` | `inject` |

---

## 기능 플래그```toml
[dependencies]
lacuna-rs = { version = "0.4", features = ["inject", "stack-spoof", "veh"] }
기능설명프레임 포인터 필요 여부
syscalls (기본값)SSN 해석 + JIT 스텁 생성아니요
inject (기본값)섹션 기반 APC 인젝션 (inject::inject_sapc)아니요
vehVEH + 하드웨어 브레이크포인트 파라미터 암호화아니요
stack-spoofLACUNA 고스트 프레임 체인 + 스택 스톰프예
no-stdno_std 모드 (실험적)아니요

기능이 활성화되지 않은 경우 스캐닝/PE/NT 계층만 사용할 수 있습니다.


빠른 시작

고스트 영역 스캔```sh

cargo run --example scan

### 고스트 프레임 체인 빌드 + 검증```sh
cargo run --example verify --features stack-spoof

섹션 + APC를 통한 셸코드 주입

주입 증명 주입 증명 -- 페이로드가 대상 프로세스 내부에서 실행되었습니다.```sh cargo run --example inject --features inject,stack-spoof,veh -- <sc.bin>

`--verbose`를 추가하면 VEH 진단 출력(스택 덤프, 레지스터 출력)을 활성화합니다:```sh
cargo run --example inject --features inject,stack-spoof,veh -- <pid> <sc.bin> --verbose

스레드 스코어링 알고리즘

inject_sapc는 대상의 스레드 순위를 매긴다. 가장 강력한 순위 신호는 스레드가 윈도우를 소유하는지 여부이다:

  • 윈도우 소유 (GUI/메시지 펌프 스레드) — EnumWindows + GetWindowThreadProcessId. GUI 스레드는 메시지 루프에서 alertable 대기 상태에 머무르므로, 큐에 들어간 APC가 그곳에서 가장 먼저 실행된다. +1000.
  • 사이클 시간 — NtQueryInformationThread(ThreadCycleTime); 실행 중인 빌드에서 해당 클래스를 사용할 수 없으면 ThreadTimes로 폴백한다 (examples/tprobe.rs 참조). 정규화되어 더해진다.
  • CPU 시간 — NtQueryInformationThread(ThreadTimes) — 커널 + 사용자 시간.
  • 서스펜드 카운트 — NtQueryInformationThread(ThreadSuspendCount). 서스펜드된 스레드는 APC를 실행할 수 없다: 서스펜드 카운트당 −2000.
  • 우선순위 — NtQueryInformationThread(ThreadBasicInformation). 우선순위 8–10은 +150; 범위를 벗어난 우선순위는 −300.

필터링. 서스펜드된 스레드와 비-alertable 커널 대기에 머무른 스레드 — WrDelayExecution (Sleep/NtDelayExecution) 또는 WrSuspended, NtQuerySystemInformation(SystemProcessInformation)에서 읽음 — 는 완전히 제외된다 (APC를 절대 받을 수 없다). 대기 이유는 그 외에는 진단용으로만 사용된다: 후보별로 State: / Reason: / Alert:로 로깅되지만, 점수에는 영향을 주지 않는다. KWAIT_REASON은 alertability를 인코딩하지 않기 때문이다 (alertable 대기와 비-alertable 객체 대기는 동일한 값을 보고한다 — examples/tprobe.rs 스타일 프로빙으로 검증됨).

후보들은 정렬되고 (최고 점수, 동점 시 GUI 우선, 그다음 사이클) MAX_APC_CANDIDATES (10)로 잘린다 — 열거/로깅용으로.

열거 ≠ 주입. 나열되는 스레드 수는 주입되는 수와 분리되어 있다. MAX_APC_CANDIDATES (10)는 순위가 매겨지고 로깅되는 수를 제어하고; APC_INJECT_THREADS (5)는 실제로 APC를 받는 수를 제어한다.

여러 스레드에 주입한 후 재시도하는 이유: 사용자 모드는 어떤 스레드가 alertable 대기 상태인지 알 수 없고, 선택된 단일 스레드가 대개 그런 상태에 진입하지 않으므로 APC가 절대 발화되지 않을 수 있다. 따라서 inject_sapc는 여러 스레드에 APC를 큐에 넣은 다음, 공유 게이트 워드가 0에서 1로 바뀔 때까지 — 즉 일부 대상 스레드가 실제로 페이로드를 실행할 때까지 — 짧은 루프(~80 ms)로 재큐잉한다. 이때 발생 여부를 로깅한다: payload claimed the gate (실행됨) vs gate NOT claimed.

한 번만 실행하는 이유: 동일한 셸코드/컨텍스트가 각 스레드에 큐잉되므로, 보호 장치가 없으면 재진입 불가능한 페이로드가 동시에 실행되어 호스트를 크래시시킬 수 있다 (측정: 3회 동시 실행). 재진입 게이트 (USE_REENTRANCY_GATE, 기본 활성화)는 APC를 받는 스레드 수 (또는 재시도 라운드 수)에 관계없이 페이로드 본문이 정확히 한 번 실행되도록 만든다 (측정: 정확히 1회). 페이로드가 명시적으로 동시성 안전하지 않은 한 게이트를 켜 두어라.


라이브러리로 사용하기

기본: 스캔 + SSN 해석```rust

use lacuna::{scan, nt, win::get_module};

let ntdll = get_module(b"ntdll.dll\0");

// Scan for ghost regions let mut ghosts = [scan::Ghost::default(); 512]; let n = scan::scan_ghosts(ntdll, &[b"NtAllocateVirtualMemory\0"], &mut ghosts); println!("{} ghost regions in ntdll", n);

// Resolve SSN + syscall;ret for a specific function let (ssn, syscall_ret) = nt::resolve(ntdll, b"NtOpenProcess\0"); println!("NtOpenProcess: ssn={:#x}, syscall;ret={:#x}", ssn, syscall_ret);

### 모든 NT API에 대한 간접 시스템 호출 스텁 생성```rust
use lacuna::{nt, stub, win::{get_module, HMODULE}};

let ntdll: HMODULE = get_module(b"ntdll.dll\0");

// Resolve SSN + the function's own syscall;ret address
let (ssn, syscall_ret) = nt::resolve(ntdll, b"NtAllocateVirtualMemory\0");
assert!(ssn != nt::SSN_INVALID && syscall_ret != 0);

// JIT-emit a stub: mov r10,rcx; mov eax,SSN; jmp [syscall;ret]
// (or jmp [ghost_gadget] -> JMP [RBX] -> syscall;ret if build_chain was called)
let stub = stub::make_stub(ssn, syscall_ret).expect("stub alloc failed");

// Cast to the matching function pointer type and call
let alloc_vm: unsafe extern "system" fn(
    win::HANDLE, *mut win::PVOID, usize, *mut usize, win::ULONG, win::ULONG,
) -> win::NTSTATUS = unsafe { core::mem::transmute(stub.as_fn()) };

고스트 프레임 체인 구축 + VEH 등록```rust

// Scan ntdll/kernelbase/wow64/win32u for ghost regions and construct // the six-layer fake call stack. Sets G_GHOST_GADGET so stubs route // through JMP [RBX] in a signed DLL. lacuna::chain::build_chain();

// Register VEH handlers (param encryption + chain guard) let _veh = lacuna::veh::VehGuard::register().expect("VEH registration failed");

// Optional: enable verbose diagnostics at runtime lacuna::veh::set_verbose(true);

### 민감한 시스템 콜을 스택 스푸핑 + 파라미터 암호화로 래핑하기```rust
use lacuna::win::HANDLE;
use core::ptr;

let mut h_proc: HANDLE = ptr::null_mut();
let key: u64 = 0xCAFE_1337;

// Plant ghost frames (replaces return addresses with signed-DLL ghosts)
lacuna::chain::stomp_plant();

// Arm DR0 on the syscall;ret -- VEH will XOR-decrypt params at the boundary
lacuna::veh::pcrypt_arm(key, syscall_ret, true);

// Call through the indirect stub -- RIP lands inside ntdll at kernel entry
let _status = unsafe { open_proc(/* XOR-encrypted params */) };

// Always disarm before any non-protected call
lacuna::veh::pcrypt_disarm();
lacuna::chain::stomp_restore();

OPSEC: 고스트 프레임 뒤에 무엇을 숨길 것인가

모든 API 호출이 전체 LACUNA 처리를 필요로 하는 것은 아니다. 핵심 원칙은: EDR이 인젝션 또는 포스트 익스플로잇 활동으로 상관분석할 호출을 숨겨라이다.

간접 시스템 콜 + 고스트 프레임 + 파라미터 암호화 뒤에 있어야 하는 것

다음은 EDR이 후킹하고 상관분석하는 "왕관의 보석" NT 시스템 콜이다:

도구 다운로드