Sigreturn-Oriented Programming (SROP)
Notes on SROP: forging a sigcontext frame so a single sigreturn sets every register at once.
예전에는 셸코드를 직접 실행시켜 시스템 콜을 호출했다.
하지만 NX 가 등장하면서 스택/힙 같은 데이터 영역에서의 코드 실행이 막혔다. 그래서 공격 기법은 이런 순서로 발전해 왔다.
셸코드 직접 실행
→ NX 로 차단
→ RTL (Return-to-Libc)
→ ROP (Return-Oriented Programming)
→ SROP (Sigreturn-Oriented Programming)
SROP 는 ROP 를 기반으로, 리눅스의 시그널 처리 과정을 악용하는 기법이다.
시그널이란
시그널은 프로세스에 보내는 “이벤트 알림”이다.
| 시그널 | 의미 |
|---|---|
| SIGINT | Ctrl+C 입력 |
| SIGSEGV | 잘못된 메모리 접근 |
| SIGALRM | alarm 시간 초과 |
| SIGKILL | 강제 종료 |
| SIGCHLD | 자식 프로세스 종료 |
운영체제는 크게 두 영역으로 나뉜다. 일반 프로그램이 도는 User Mode 와 OS 커널이 도는 Kernel Mode 다.
프로그램은 평소 User Mode 에서 실행되지만, 시그널이 발생하면 커널이 개입해야 한다.
시그널 발생 → Kernel Mode 진입 → 커널이 시그널 처리 준비
→ User Mode 복귀 → 등록된 Signal Handler 실행
여기서 중요한 점은, 시그널을 처리하려면 커널이 현재 프로세스의 레지스터와 스택 상태를 저장했다가 되돌려 놓아야 한다는 것이다.
커널 내부의 시그널 처리 흐름
시그널 발생
→ do_signal()
→ get_signal()
→ handle_signal()
→ setup_rt_frame()
→ User Mode 복귀
→ Signal Handler 실행
각 함수의 역할은 다음과 같다.
| 함수 | 역할 |
|---|---|
do_signal() / arch_do_signal_or_restart() | 시그널 처리의 시작점 |
get_signal() | 처리할 시그널이 있는지 확인 |
handle_signal() | 실제 Signal Handler 실행 준비 |
setup_rt_frame() | User Stack 에 Signal Frame 구성 |
sigreturn | 시그널 처리 후 원래 상태로 복귀 |
setup_rt_frame 이 중요한 이유
Signal Handler 를 실행하려면 커널은 다음 정보를 저장해 두어야 한다.
- 현재 RIP/EIP
- 현재 RSP/ESP
- 레지스터 값들
- 시그널 정보
- 원래 실행 위치
이 정보는 User Stack 에 저장되고, 이 구조를 보통 Signal Frame 이라고 부른다. setup_rt_frame() 이 이 Signal Frame 을 만든다.
간단히 말하면, 커널은 현재 프로세스 상태를 스택에 저장해 두었다가 시그널 핸들러가 끝나면 그 상태로 되돌린다.
sigreturn
sigreturn 은 시그널 핸들러가 끝난 뒤 원래 실행 상태로 돌아가기 위한 시스템 콜이다.
시그널 발생
→ 커널 개입
→ 현재 레지스터/스택 상태 저장
→ Signal Handler 실행
→ sigreturn 호출
→ 저장된 상태 복원
→ 원래 코드로 복귀
즉 sigreturn 의 역할은 스택에 저장된 CPU 상태를 읽어서 레지스터를 복원하는 것이다.
컨텍스트 스위칭 관점
Context Switching 은 실행 상태가 바뀌는 것을 말한다.
기존 프로그램 실행
→ 시그널 발생
→ 커널 코드 실행
→ 시그널 핸들러 실행
→ 원래 프로그램으로 복귀
이 과정에서 커널은 기존 상태를 잊으면 안 되기 때문에 레지스터 정보를 전부 저장해 둔다. 그래서 사실상 sigreturn 은 컨텍스트 스위칭 때문에 존재하는 것이라고 볼 수 있다.
공격으로 이어지는 지점
핵심은 이것이다. sigreturn 은 스택에 있는 값을 그대로 믿고 레지스터에 복원한다. 그 스택이 진짜 커널이 만든 Signal Frame 인지는 검증하지 않는다.
따라서 공격자가 스택에 가짜 Signal Frame 을 직접 써 놓고 sigreturn 으로 넘어가면, RIP·RSP 를 포함한 모든 레지스터를 한 번에 원하는 값으로 세팅할 수 있다.
일반 ROP 는 레지스터 하나를 세팅하려고 가젯을 하나씩 찾아 이어 붙여야 하지만, SROP 는 syscall 가젯 하나와 스택에 쓸 수 있는 조건만 있으면 된다. 그래서 가젯이 거의 없는 정적 바이너리에서 특히 강력하다.