2P by GN⁺ | ★ favorite | 댓글 1개
  • Windows 커널 드라이버도 Rust로 작성할 수 있음을 보이기 위해, 임의 스레드의 우선순위를 바꾸는 Booster WDM 드라이버를 구현함
  • 빌드 환경은 WDK 또는 EWDK, LLVM/Clang, windows-drivers-rs 기반 WDK crates, build.rsCargo.toml 설정이 맞물려야 함
  • 커널 공간에서는 표준 라이브러리를 쓸 수 없어 #![no_std], WDK allocator, panic handler, unsafe FFI 호출을 조합해야 함
  • 드라이버는 \Device\Booster\??\Booster를 만들고, IRP_MJ_WRITE에서 받은 ThreadData스레드 우선순위를 1~31 범위에서 변경함
  • Rust로 Windows 커널 드라이버 작성은 가능하지만, WDK crates가 0.3 단계라 더 안전한 래퍼와 적은 unsafe 코드가 필요함

Rust로 구현한 Booster WDM 드라이버

  • 예제는 Windows 커널 프로그래밍에서 쓰는 “Booster” 드라이버를 Rust로 옮긴 것임
    • 목적은 임의 스레드의 우선순위를 원하는 값으로 바꾸는 것임
    • 드라이버 모델은 WDM
  • Rust 생태계는 컴파일 시점의 메모리 안전성, 동시성 안전성, cargo 빌드 시스템, crates 생태계를 갖추고 있음
  • C 타입을 Rust에서 직접 다루면 코드가 장황해질 수 있음
    • 적절한 래퍼와 매크로를 쓰면 이 부담을 줄일 수 있음

빌드 준비와 Cargo 설정

  • 드라이버 빌드를 준비하려면 Windows Drivers-rs를 참고하고, WDK 또는 EWDK를 설치해야 함
  • LLVM 설치도 필요하며, Clang 컴파일러에 접근하기 위해 사용함
  • 새 Rust 라이브러리 프로젝트를 만들며, 커널 드라이버는 기술적으로 커널 공간에 로드되는 DLL임
cargo new --lib booster
  • build.rs는 cargo가 CRT에 정적 링크하도록 설정하고 WDK 바이너리 빌드를 구성함
fn main() -> Result<(), wdk_build::ConfigError> {
    std::env::set_var("CARGO_CFG_TARGET_FEATURE", "crt-static");
    wdk_build::configure_wdk_binary_build()
}
  • Cargo.toml에는 WDM 드라이버 모델, cdylib crate 타입, WDK 관련 의존성을 넣음
    • 주요 crates는 wdk, wdk-macros, wdk-alloc, wdk-panic, wdk-sys
    • dev/release 프로필에는 panic = "abort"lto = true를 설정함
    • wdkwdk-sys에는 선택적으로 nightly feature가 있음

표준 라이브러리 없이 커널 코드 작성

  • 커널에는 Rust 표준 라이브러리가 없으므로 #![no_std]를 사용함
  • wdk_sys는 저수준 커널 함수와의 상호운용을 맡고, wdk는 더 높은 수준의 래퍼를 제공함
  • VecString은 표준 라이브러리의 일부처럼 보이지만, 실제로는 alloc 모듈의 타입을 사용할 수 있음
    • 이를 쓰려면 전역 할당자가 필요함
    • WDK crates가 제공하는 WdkAllocator#[global_allocator]로 지정함
  • WdkAllocatorExAllocatePool2ExFreePool을 사용해 할당을 관리함
  • 표준 라이브러리가 없기 때문에 allocator 지원과 panic handler를 위해 wdk_panicalloc 외부 crate를 추가함

DriverEntry와 장치 초기화

  • Windows 커널 드라이버의 진입점은 DriverEntry
    • Rust 함수명은 관례에 맞춰 driver_entry로 두고, #[export_name = "DriverEntry"]로 링커가 찾는 이름을 지정함
  • println! 매크로는 DbgPrint 호출로 재구현되어 있어, C/C++에서 DbgPrint를 쓰는 것처럼 커널 디버그 출력에 사용할 수 있음
  • UNICODE_STRINGprintln!에서 직접 지원되지 않으므로 unicode_to_string 함수로 Rust String으로 변환함
  • 장치 객체는 IoCreateDevice\Device\Booster에 생성함
    • 실패하면 오류 상태를 출력하고 해당 NTSTATUS를 반환함
    • 성공 여부 판단에는 WDK의 NT_SUCCESS 매크로와 유사한 nt_success를 사용함
  • 표준 CreateFile 호출로 장치를 열 수 있도록 IoCreateSymbolicLink\??\Booster 심볼릭 링크를 만듦
    • 심볼릭 링크 생성에 실패하면 장치 객체를 삭제하고 실패 상태를 반환함
  • 장치 객체는 Buffered I/O를 사용하도록 설정함
    • DriverUnloadboost_unload로 설정함
    • IRP_MJ_CREATEIRP_MJ_CLOSEboost_create_close로 처리함
    • IRP_MJ_WRITEboost_write로 처리함
  • 콜백 존재 여부는 Rust의 Option<> 타입으로 표현됨

요청 처리와 스레드 우선순위 변경

  • 언로드 루틴은 IoDeleteSymbolicLinkIoDeleteDevice를 호출해 심볼릭 링크와 장치 객체를 정리함
  • IRP_MJ_CREATEIRP_MJ_CLOSE 처리는 단순함
    • IRP의 IoStatus.StatusSTATUS_SUCCESS로 설정함
    • IoStatus.Information을 0으로 설정함
    • IofCompleteRequest로 요청을 완료함
  • IoStatusIO_STATUS_BLOCK이며, Status 접근 시 자동 생성된 union 멤버를 거쳐야 해 코드가 보기 좋지 않음
    • 해당 정의는 더 확인할 부분으로 남아 있음
  • 실제 우선순위 변경은 IRP_MJ_WRITE 핸들러에서 수행함
  • 클라이언트가 드라이버에 넘기는 구조체는 C/C++와 같은 메모리 배치를 위해 #[repr(C)]를 사용함
#[repr(C)]
struct ThreadData {
    pub thread_id: u32,
    pub priority: i32,
}
  • boost_write는 Buffered I/O로 전달된 SystemBufferThreadData 포인터로 해석함
  • 오류 검사는 다음 조건을 포함함
    • 데이터 포인터가 null이면 STATUS_INVALID_PARAMETER
    • 우선순위가 1보다 작거나 31보다 크면 STATUS_INVALID_PARAMETER
  • PsLookupThreadByThreadId로 스레드 객체를 찾음
    • 실패하면 해당 스레드 ID가 없을 가능성이 있으며 처리 루프를 빠져나감
  • 스레드를 찾으면 KeSetPriorityThread로 우선순위를 설정하고 ObfDereferenceObject로 참조를 해제함
  • 요청 완료 시 IRP 상태와 정보 필드를 설정한 뒤 IofCompleteRequest를 호출함

서명, 설치, 테스트

  • INF 또는 INX 파일이 있으면 crates가 드라이버 서명을 지원하는 것으로 보이지만, 이 예제는 INF를 사용하지 않아 수동 서명이 필요함
  • 프로젝트 루트에서 다음 명령으로 빌드 산출물을 서명할 수 있음
signtool sign /n wdk /fd sha256 target\debug\booster.dll
  • /n wdk는 Visual Studio가 드라이버 빌드 시 보통 자동 생성하는 WDK 테스트 인증서를 사용함
  • 빌드 산출물 확장자는 DLL임
    • 현재 cargo build 과정에서 자동으로 확장자를 바꾸는 방법은 없음
    • INF/INX를 사용하면 확장자가 SYS로 바뀜
    • 확장자는 직접 바꾸거나 DLL 그대로 둘 수 있음
  • 테스트 서명이 켜진 머신에서 관리자 권한 명령 프롬프트로 sc.exe를 사용해 소프트웨어 드라이버처럼 설치할 수 있음
sc.exe sc create booster type= kernel binPath= c:\path_to_driver_file
sc.exe start booster
  • 테스트 클라이언트는 기존 C++ 애플리케이션을 사용함
    • CreateFile(L"\\\\.\\Booster", GENERIC_WRITE, ...)로 장치를 엶
    • ThreadData에 스레드 ID와 우선순위를 넣고 WriteFile로 전달함
    • 예시는 ID 9408 스레드의 우선순위를 26으로 바꾸는 테스트임

남은 과제와 참고 자료

  • Rust로 커널 드라이버를 작성하는 것은 가능함
  • WDK crates는 버전 0.3 단계라 아직 개선 여지가 있음
  • Rust의 장점을 제대로 얻으려면 안전한 래퍼가 더 필요함
    • 코드가 덜 장황해야 함
    • unsafe 블록이 줄어야 함
    • Rust가 제공하는 안전성 이점을 더 잘 활용해야 함
  • KMDF Rust 드라이버 샘플은 Windows-rust-driver-samples에 있음
  • 예제 코드는 Booster 저장소에서 확인할 수 있음
  • Rust 학습 자료는 https://trainsec.net에 있음

댓글과 토론

Hacker News 의견들
  • 애플리케이션별로 경로 재매핑 규칙을 설정할 수 있는 파일시스템 필터 드라이버를 만들 생각을 했었음
    예를 들면 %userprofile%\.vscode -> %appdata%\vscode, %CSIDL_MYDOCUMENTS%\Call of Duty -> %userprofile%\Saved Games\Call of Duty 같은 식임
    문서 폴더와 홈 디렉터리가 이미 정해진 위치가 있는 잡동사니로 가득 차는 게 너무 화가 나서 Rust로 필터 드라이버 프로젝트 뼈대를 만들고 미니필터 문서를 읽어봤지만, 작업량을 보고 포기했음
    Windows 시스템은 결국 쓰레기로 가득 찰 수밖에 없다는 사실을 받아들이게 됨

    • 사용자 파일시스템을 쓰레기장처럼 쓰는 운영체제는 Windows만이 아님
      .DS_Store, .fseventsd, ._xxxx 같은 파일을 몇 번이나 지워도 Apple이 계속 여기저기 뿌려댐
      그래도 macOS는 애플리케이션 설치 위치와 사용자 문서 위치가 대체로 하나씩 있고, 대부분의 앱이 어느 정도 이를 지키는 편임
      대신 ~/Library 같은 지정된 쓰레기장이 있고, 거기에 내가 필요한지 알 수 없는 잡동사니가 잔뜩 들어 있음
    • 내 Documents 폴더가 이상하게 비어 있어서 봤더니 Shortcut to Documents (OneDrive - Personal)이었음
      이제 그 쓰레기들이 기기 간 동기화까지 되고 있어서 참 좋음
    • 이건 사용자 모드에서도 여러 방식으로 가능함
      Detours 라이브러리는 프로세스 실행 시 사용자 DLL을 붙여서 파일시스템 API에 대한 Win32 호출을 후킹할 수 있게 해줌
      내장된 “호환성 shim”도 거의 문서화되어 있진 않지만 비슷하게 동작하며, 호환성 플래그를 켜고 파일·레지스트리 경로를 리다이렉트할 수 있음
      특정 EXE에 대해서만 휴리스틱 기반으로 발동하도록 설계된 점도 편리함
      App-V나 이후 Docker 컨테이너 같은 기술을 지원하려고 Windows에는 파일시스템과 레지스트리, 그 밖의 NT 커널 네임스페이스를 가상화하는 API 표면이 있음
      그 외에도 User Mode Filesystem이 있고, 내가 모르거나 잊은 접근법도 더 있을 듯함
    • My Documents라는 유독성 폐기물장은 완전히 무시하고, 실제로 신경 쓰는 파일은 더 짧은 경로의 다른 위치에 둠
      데이터 위치를 하드코딩한 프로그램 한두 개는 디렉터리 접합으로 리다이렉트했을 수도 있음
    • Microsoft/Windows 자체도 홈 폴더에 잡동사니를 잔뜩 넣고, 더 나쁘게는 이름도 지나치게 김
      ntuser.ini, ntuser.dat.LOG1, ntuser.dat.LOG2, NTUSER.DAT, NTUSER.DAT{b2352f18-cdbf-1122-8680-002248483d79}.TM.blf, NTUSER.DAT{b2352f18-cdbf-1122-8680-002248483d79}.TMContainer00000000000000000001.regtrans-ms, NTUSER.DAT{b2352f18-cdbf-1122-8680-002248483d79}.TMContainer00000000000000000002.regtrans-ms 같은 것들임
  • 반쯤 관련해서, Windows 커널에서 Rust 사용 현황에 대한 최신 정보가 있는지 궁금함
    거의 2년 전에 “시스템 호출을 포함해 36,000줄의 코드”라고 했는데 [1], 그 프로젝트가 어떻게 진행됐는지 알고 싶음
    [1] https://www.thurrott.com/windows/282471/microsoft-is-rewriti...

  • 흥미로움. 내가 Rust로 많이 해본 임베디드 드라이버와는 꽤 달라 보임
    그쪽은 대부분 레지스터 읽기·쓰기, 비트 시프트, DMA, 데이터시트 참고가 중심임

  • 여기 코드가 사실상 문법만 다른 C처럼 보임. 모든 함수가 unsafe로 표시되어 있고, 모든 자원을 수동으로 관리함
    직설적으로 말해 미안하지만, 이러면 Rust를 쓰는 의미가 뭔지 모르겠음

    • 그리고 어떻게 놓쳤는지 모르겠지만, string_to_ustring에서 반환된 UNICODE_STRING을 사용하려는 시도는 확정적인 해제 후 사용
      Windows 커널 코드를 쓰고 싶다면 여기서 시작하면 안 됨
    • 안타깝지만 단순 예제는 사실상 전부 배관 코드라서, 그런 부분은 모두 안전하지 않은 네이티브 인터페이스를 건드리게 됨
      실제로 흥미로운 일을 하는 드라이버라면 경계부에만 unsafe 인터페이스를 두고, 내부는 더 전형적인 Rust 코드로 작성할 수 있음
    • 여기에 전부 대문자인 타입 이름도 엄청 많음
      정말 표준 Rust 명명 규칙을 다 버리고, 오래된 Windows식 “이름으로 타입을 표현하는” 관습을 받아들이려는 건가?
  • 25년쯤 전에 Windows용 특정 드라이버를 작성해야 했음
    그때는 이미 Linux로 완전히 넘어간 상태라 작성과 빌드에 Windows를 쓰기 싫어서, MSYS로 빌드되게 만들려고 꽤 애썼음
    결국 성공했고 드라이버도 잘 동작했음
    실제로 로드되게 하려면 결과물 PE 파일(.sys)에 패처를 써야 했던 것 같음
    재미있는 시절이었음

  • 글도 좋지만 블로그 디자인이 더 인상적임
    깔끔하고 직관적이며 눈이 편하고 즉시 로드됨

    • 나도 같았음. 블로그가 어떤 기술로 되어 있는지 다시 확인해봐야 했음
      보니 WordPress.com인 것 같음
      캐싱과 CDN에 많이 의존하는 듯함
      10년 전에 내 WordPress 블로그를 계속 유지하지 않은 게 아쉬워짐