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

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

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

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

도구 디렉토리

카테고리

모든 카테고리 보기
Loading categories
Unwinder — 콜 스택 스푸핑 for Rust | Kitploit
도구/GitHubGitHub/kudaes/unwinder
IDS/IPS EvasionPost-ExploitationRed TeamingPayload DevelopmentAdversarial Attack
GitHubkudaes/unwinder

Unwinder

콜 스택 스푸핑 for Rust

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

인기

모두 보기 →

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

모든 도구 탐색

도구 컬렉션을 둘러보세요

모든 도구 보기 →
공유

목차

  • SilentMoonWalk
    • 설명
    • 크레딧
    • 사용법
      • call_function!() 매크로
      • indirect_syscall!() 매크로
      • 매개변수 전달
    • 예제
      • kernel32.dll!Sleep() 호출
      • kernel32.dll!OpenProcess() 호출
      • 간접 시스템 콜로 NtDelayExecution() 호출
      • 매크로 호출 연결
    • 고려 사항
      • 초기 프레임
      • PoC
  • 스택 교체
    • 설명
    • 사용법
    • 실용 예제
    • 참고 사항

SilentMoonWalk

설명

Unwinder는 SilentMoonWalk 기법의 완전한 무기화(weaponization)를 제공하여 Rust에서 완전하고 안정적인 호출 스택 스푸핑(call stack spoofing)을 달성할 수 있게 합니다.

이 기법은 다음과 같은 특징을 가집니다:

  • 최대 11개의 매개변수를 사용하여 임의의 함수를 실행할 수 있습니다.
  • 최대 11개의 매개변수를 사용하여 간접 시스템 콜(indirect syscall)을 실행할 수 있습니다(추가 힙 할당 없음).
  • 이 크레이트는 이를 통해 호출된 함수가 반환하는 값을 검색할 수 있게 합니다.
  • 스푸핑 프로세스는 호출 스택 크기를 늘리지 않고 여러 번 연결할 수 있습니다.
  • 스푸핑 프로세스의 효율성을 높이기 위해 TLS가 사용됩니다.
  • 크레이트에 필요한 Windows API 호출을 위해 dinvoke_rs가 사용됩니다.

크레딧

SilentMoonWalk 기법을 만든 이들에게 감사를 전합니다:

  • KlezVirus
  • Waldo-IRC
  • Trickster0

그리고 물론 이 프로젝트 전체에 영감을 준 Twitter 스레드에 대해 namazso에게 큰 감사를 보냅니다.

사용법

프로젝트에 이 크레이트를 가져오려면 cargo.toml에 다음 줄을 추가하고 release 모드로 컴파일하세요:

[dependencies]
unwinder = "=0.1.4"

이 크레이트의 주요 기능은 두 개의 매크로로 래핑되어 있습니다:

  • call_function!() 매크로는 깨끗한 호출 스택으로 임의의 함수를 실행할 수 있게 합니다.
  • indirect_syscall!() 매크로는 깨끗한 호출 스택으로 지정된 (간접) 시스템 콜을 실행합니다.

이 매크로들을 사용하려면 std::ffi::c_void 데이터 타입을 가져와야 합니다.

두 매크로 모두 실행된 함수가 반환한 값을 검색하는 데 사용할 수 있는 *mut c_void를 반환합니다. 자세한 내용은 예제 섹션을 참조하세요.

call_function 매크로

이 매크로는 깨끗한 호출 스택으로 원하는 함수를 호출하는 데 사용됩니다. 매크로는 다음 매개변수를 기대합니다:

  • 첫 번째 매개변수는 호출 스택을 스푸핑한 후 호출할 메모리 주소입니다. 이 매개변수는 usize, isize 또는 포인터로 전달해야 합니다.
  • 두 번째 매개변수는 시작 함수 프레임을 유지할지 여부를 나타내는 bool입니다. 확실하지 않으면 false로 설정하세요. 그러면 항상 좋은 호출 스택이 보장됩니다.
  • 그 다음 매개변수들은 호출 스택이 스푸핑된 후 함수에 전달할 인수들입니다.

indirect_syscall 매크로

이 매크로는 깨끗한 호출 스택으로 원하는 간접 시스템 콜을 수행하는 데 사용됩니다. 매크로는 다음 매개변수를 기대합니다:

  • 첫 번째 매개변수는 시스템 콜을 실행하려는 NT 함수의 이름을 포함하는 문자열입니다.
  • 두 번째 매개변수는 시작 함수 프레임을 유지할지 여부를 나타내는 bool입니다. 확실하지 않으면 false로 설정하세요. 그러면 항상 좋은 호출 스택이 보장됩니다.
  • 그 다음 매개변수들은 NT 함수에 전달할 인수들입니다.

매개변수 전달

이 두 매크로에 다양한 타입의 인수를 전달하려면 다음 사항을 고려해야 합니다:

  • usize로 변환할 수 있는 모든 기본 데이터 타입(u8-u64, i8-i64, bool 등)은 매크로에 직접 전달할 수 있습니다.
  • 크기가 8, 16, 32, 64비트인 구조체(struct)와 공용체(union)는 동일한 크기의 정수처럼 전달됩니다.
  • 64비트보다 큰 크기의 구조체와 공용체는 포인터로 전달해야 합니다.
  • 문자열(&str 및 String)은 포인터로 전달해야 합니다.
  • 널 포인터(ptr::null(), ptr::null_mut() 등)는 0으로 전달됩니다(u8, u16, i32 또는 다른 타입이든 상관없습니다).
  • 부동소수점 및 배정밀도(double-precision) 매개변수는 현재 지원되지 않습니다.
  • 다른 모든 데이터 타입은 포인터로 전달해야 합니다.

예제

Sleep 호출

let k32 = dinvoke_rs::dinvoke::get_module_base_address("kernel32.dll");
let sleep = dinvoke_rs::dinvoke::get_function_address(k32, "Sleep"); // Memory address of kernel32.dll!Sleep() 
let miliseconds = 1000i32;
unwinder::call_function!(sleep, false, miliseconds);

OpenProcess 호출

let k32 = dinvoke_rs::dinvoke::get_module_base_address("kernel32.dll"); 
let open_process: isize = dinvoke_rs::dinvoke::get_function_address(k32, "Openprocess");
let desired_access: u32 = 0x1000;
let inherit = 0i32;
let pid = 20628i32;
let handle = unwinder::call_function!(open_process, false, desired_access, inherit, pid); // returns *mut c_void
let handle: HANDLE = std::mem::transmute(handle);
println!("Handle id: {:x}", handle.0);

매크로는 *mut c_void를 반환하며, 두 데이터 타입의 크기가 같으므로 이를 HANDLE로 직접 변환할 수 있습니다. 이를 통해 OpenProcess가 반환한 값, 즉 대상 프로세스에 대한 새 핸들에 접근할 수 있습니다.

간접 시스템 콜로 NtDelayExecution 호출

let large = 0x8000000000000000 as u64; // Sleep indefinitely
let large: *mut i64 = std::mem::transmute(&large);
let alertable = false;
let ntstatus = unwinder::indirect_syscall!("NtDelayExecution", false, alertable, large); // returns *mut c_void
println!("ntstatus: {:x}", ntstatus as i32);

매크로는 *mut c_void를 반환하며, 이를 사용하여 NtDelayExecution이 반환한 NTSTATUS 값을 검색할 수 있습니다.

매크로 호출 연결

스푸핑 프로세스는 비정상적인 호출 스택 크기 증가 없이 몇 번이든 연결할 수 있습니다. 실행 흐름도 보존됩니다. 다음 코드는 이에 대한 예입니다:

fn main()
{
	function_a();
}

fn function_a()
{
	unsafe
	{
		let func_b = function_b as usize;
		call_function!(func_b, false);
		println!("function_a done.");
	}
}

fn function_b()
{
	unsafe
	{
		let func_c = function_c as usize;
		call_function!(func_c, false);
		println!("function_b done.")
	}
}

fn function_c()
{
	unsafe
	{
		let large = 0x0000000000000000 as u64; // Don't sleep so we return to function_b, allowing to check the execution flow preservation.
		let large: *mut i64 = std::mem::transmute(&large);
		let alertable = false;
		let ntstatus = unwinder::indirect_syscall!("NtDelayExecution", false, alertable, large);
		println!("ntstatus: {:x}", (ntstatus as usize) as i32); //NTSTATUS is a i32, although that second casting is not really required in this case.
	}
}

고려 사항

초기 프레임

두 매크로 모두 두 번째 매개변수를 true로 설정하면, 스푸핑 프로세스는 합법성을 높이기 위해 스레드 시작 주소의 프레임을 호출 스택에 유지하려고 시도합니다.

주 모듈을 유지한 채 스푸핑된 호출 스택.

때때로 스레드의 시작 함수는 후속 함수에 대한 call을 수행하지 않습니다(예: 대신 jmp 명령이 실행됨). 즉, 스택에 반환 주소가 푸시되지 않습니다. 이 시나리오에서는(그리고 두 번째 매개변수를 false로 설정한 경우에도) 스푸핑된 호출 스택은 BaseThreadInitThunk의 프레임에서 시작됩니다.

주 모듈 없이 스푸핑된 호출 스택.

PoC

기법의 구현을 테스트하기 위해 /threads 플래그와 함께 PE-sieve가 사용되었습니다. 테스트 결과는 이 크레이트의 기능을 사용할 때 호출 스택 검사가 페이로드의 존재를 드러내지 않음을 보여줍니다. 두 번째 이미지에서 볼 수 있듯이 unwinder를 사용하지 않으면 페이로드가 탐지됩니다.

unwinder를 사용한 경우의 PE-sieve 결과. unwinder를 사용하지 않은 경우의 PE-sieve 결과.

스택 교체

기법 설명

이 기법은 SilentMoonWalk의 대안이 되는 호출 스택 스푸핑 방식으로, 프로그램 실행 중에 깨끗한 호출 스택을 유지할 수 있게 합니다. 이 기법의 기본 아이디어는 모듈 내부에서 호출되는 각 함수가 이전에 푸시된 반환 주소를 처리하고, 런타임에 스푸핑할 반환 주소와 동일한 프레임 크기를 가진 합법적인 함수를 찾는 것입니다. 동일한 프레임 크기의 정상적인 함수를 찾으면 그 함수 내부의 오프셋을 계산하고, 최종 주소를 사용하여 마지막 반환 주소를 교체하여 호출 스택에서 비정상적인 항목을 숨기고 스택을 계속 해제(unwind) 가능하게 유지합니다. 원래 반환 주소는 unwinder에 저장되며, return 명령이 실행되기 전에 스택의 올바른 위치로 다시 이동되어 프로그램의 정상적인 흐름을 계속할 수 있습니다.

스택 교체

이 기능은 완전히 작동하지만 여전히 개발 및 연구 중인 실험적 기능입니다. 따라서 이 기법을 코드에 통합하려면 반드시 테스트를 수행하세요.

사용법

스택 교체 기능을 사용하려면 cargo.toml에 다음 줄을 추가하고 release 모드로 컴파일해야 합니다:

[dependencies]
unwinder = {version = "0.1.4", features = ["Experimental"]}
도구 다운로드