Manual syscalls
수동 시스템 콜은 Windows의 ntdll.dll에 있는 네이티브 API를 사용하지 않고, 직접 커스텀한 syscall stub을 통해 syscall 명령어를 실행하여 커널 함수를 호출하는 기법입니다.
Direct syscalls
일반적인 경우에서 사용자가 시스템 콜을 호출할 때는 커널 모드로 진입하는 네이티브 API를 호출하여 간접적으로 커널 모드로 진입합니다.
하지만 커널 모드로 진입하는 API 자체를 직접 빌드한다면 네이티브 API를 호출하지 않고도 커널 모드에 진입하는 것이 가능합니다.
Visual Studio에서는 어셈블리 코드를 C(++)로 작성된 프로그램으로 컴파일 할 수 있는 사용자 지정 빌드(MASM)가 존재하여 직접 시스템 콜 호출 코드를 만들 수 있습니다.
위 코드에서는 NtOpenProcess API에 대해 직접 호출을 하기 때문에 네이티브 API를 호출하지 않습니다.
다음 사진은 정상적으로 NtOpenProcess를 호출하는 파일과, 직접 syscalls를 호출하는 파일에 대해 브레이크 포인트 및 호출 스택을 확인하여 NtOpenProcess가 사용되었는지 확인한 결과입니다.


사진에서 확인할 수 있듯 정상 호출의 경우 NtOpenProcess에서 브레이크 포인트를 지정하면 실행이 멈추는 반면, 직접 시스템 호출의 경우 실행이 종료되고 호출 스택에서도 NtOpenProcess는 기록되지 않습니다.
Indirect syscalls
사용자 모드에서 커널 모드로 진입하는 네이티브 API 호출에서는 대부분 동일한 syscall stub을 따르며, 유일한 차이점으로 SSN(System Service Number)이 있습니다.
네이티브 API 종류에 상관없이 syscall stub이 동일하다는 것은 SSN을 조작하면 다른 API를 호출할 수 있다는 것을 의미합니다.
NtOpenProcess를 사용하고자 하지만 EDR이 해당 함수를 후킹하여 모니터링 중이라고 가정하겠습니다.
공격자는 NtOpenProcess의 SSN(0x26)을 설정한 뒤, EDR이 후킹하지 않는 함수(여기선 NtClose) 내부의 syscall 명령어 위치로 직접 점프합니다. 이를 통해 호출 스택에는 NtClose에서 정상적으로 syscall이 발생한 것처럼 보이지만, 커널에서는 SSN 값에 따라 실제로 NtOpenProcess가 실행됩니다.
위 코드에서 만약 call을 사용했다면 호출 스택은 main → IndirectSyscalls → ntdll!NtClose (syscall) 가 되는 반면 jmp를 사용하면 main → ntdll!NtClose (syscall) 가 됩니다.
이로 인해 공격자는 커스텀 네이티브 API 함수 내에서 사용하고자 하는 API의 SSN을 통해 API를 호출하는 동시에, EDR에서 주로 후킹하지 않는 비주류 API로 점프하여 탐지를 우회할 수 있게됩니다.
References
Last updated

