Call stack spoofing

Stack unwinding은 호출 스택을 역방향으로 정리해가며 올라가는 과정으로, EDR 솔루션이 메모리 내 악성 행위가 있는지 탐지하기 위해 사용하기도 하는 기법입니다.

언와이딩은 예외 처리 루틴을 만나거나 스택의 return address가 0x0이 될때까지 반복하는데, 공격자는 return address를 0x0으로 설정하여 언와이더를 방해하여 분석을 회피할 수 있습니다.

main() → MaliciousFunc() → SuspiciousAPI() → NtAllocateVirtualMemory() 순서대로 함수를 호출하는 프로그램이 있다고 가정할 때, 호출 스택의 모습은 다음과 같습니다.

Vaild call stack
[NtAllocateVirtualMemory]  ← 현재 실행 중 (RSP)
[SuspiciousAPI return]
[MaliciousFunc return]
[main return]

스택 언와이딩 과정에서는 네이티브 API를 호출한 SuspiciousAPI 함수를 정리하며 MaliciousFunc를 발견하게 되고, 이를 통해 악성 행위를 탐지할 수 있습니다.

공격자는 return address를 0x0으로 지정한 가짜 프레임을 생성하여 언와이딩을 강제로 중지하게 설정하고, 숨겨진 스택에서 악성 행위를 진행할 수 있습니다.

Spoofed call stack
[NtAllocateVirtualMemory]  ← 현재 실행
[BaseThreadInitThunk]      ← 가짜 함수
[RtlUserThreadStart]       ← 가짜 함수
[0x00000000]               ← 가짜 종료점
─────────────────────────  ← Stack Walk는 여기서 멈춤
[실제 악성 함수들]           ← 숨겨짐

가짜 스택 프레임을 생성하는 이유는 네이티브 API를 호출하기 위한 정상 루틴으로 위장하기 위함입니다.

CPU는 함수의 return address에 있는 주소로 점프하는 반면, 언와이더는 unwind metadata(.pdata/.data)를 기준으로 caller를 계산하여 caller frame을 추정합니다.

때문에 언와이더는 함수의 return address가 0이 아닌 상황이라면 종료하지 않고 caller frame을 추정하여 다음 루틴으로 이동합니다.

콜스택 스푸핑의 전체적인 과정에 따른 스택의 변화를 단계별로 표현하면 다음과 같습니다.

  1. implant call stack(caller)의 주소가 있는 스택 return address를 0으로 변경(언와이딩 중지 목적)

  1. 가짜 스택 프레임 생성, 네이티브 API 호출 함수의 return address에 가젯 주소 저장

  1. 가젯이 호출하는 Clean up 함수를 통해 가짜 스택 프레임 제거 및 implant call stack으로 복귀

HulkOperator의 블로그에서 콜스택 스푸핑 프로젝트에 대한 자세한 분석을 확인할 수 있고, 그의 오픈소스 프로젝트를 통해 직접 디버깅을 해볼 수 있습니다.

스푸핑을 한 네이티브 API 호출과 일반 호출의 차이점을 확인하기 위해 main.c 함수에 정상 호출 코드도 추가한 뒤, Windbg를 통해 차이를 확인합니다.

정상 MessageBoxA 호출
스푸핑 MessageBoxA 호출

References

Last updated