[Dreamhack] Background: SigReturn-Oriented Programming

#코딩노예#·2022년 7월 31일
0

Dreamhack - System Hacking

목록 보기
45/49

Signal

시그널

시그널은 프로세스에 특정 정보를 전달하는 매개체로, 우리가 프로그램을 공격할때 보았던 SIGSEGV 또한 시그널에 포함됩니다.

시그널이 발생하면 시그널에 해당하는 코드가 커널 모드에서 실행되고, 다시 유저 모드로 복귀합니다.


시그널 동작 방식

다음은 5초 후에 sig_handler를 호출하는 예제 코드 입니다.

// Name: sig_alarm.c
// Compile: gcc -o sig_alarm sig_alarm.c 

#include<stdio.h>
#include<unistd.h>
#include<signal.h>
#include<stdlib.h>
 
void sig_handler(int signum){
  printf("sig_handler called.\n");
  exit(0);
}

int main(){
  signal(SIGALRM, sig_handler);
  alarm(5);
  
  getchar();
  return 0;
}

코드를 살펴보면 signal 함수를 사용해 SIGALRM 시그널이 발생하면 sig_handler 함수를 실행합니다. 프로세스에서 이 모든 것을 처리하는 것처럼 보일 수 있으나, SIGALRM 시그널이 발생하면 커널 모드로 진입합니다. 커널 모드에서 시그널을 다 처리하고 나면 다시 유저 모드로 돌아와 프로세스의 코드를 실행합니다. 이때 유저 모드의 상태를 모두 기억하고 되돌아 올 수 있어야 합니다. 여기서 상태란, 시그널이 발생했을 때 프로세스의 메모리, 레지스터 등이 포함됩니다.

커널에서 유저 모드로 되돌아가야 하는 상황을 고려해 유저 프로세스의 상태를 저장하는 코드가 구현되어 있습니다.


do_signal

do_signal 함수는 시그널을 처리하기 위해 가장 먼저 호출되는 함수입니다.
리눅스 커널 5.8 버전 이하에서는 do_signal, 리눅스 커널 5.10이하 버전에서는 arch_do_signal, 상위 버전에서는 arch_do_signal_or_restart로 함수 이름이 변경되었습니다.


void arch_do_signal_or_restart(struct pt_regs *regs, bool has_signal)
{
	struct ksignal ksig;
	if (has_signal && get_signal(&ksig)) {
		/* Whee! Actually deliver the signal.  */
		handle_signal(&ksig, regs);
		return;
	}
	/* Did we come from a system call? */
	if (syscall_get_nr(current, regs) >= 0) {
		/* Restart the system call - no handlers present */
		switch (syscall_get_error(current, regs)) {
		case -ERESTARTNOHAND:
		case -ERESTARTSYS:
		case -ERESTARTNOINTR:
			regs->ax = regs->orig_ax;
			regs->ip -= 2;
			break;
		case -ERESTART_RESTARTBLOCK:
			regs->ax = get_nr_restart_syscall(regs);
			regs->ip -= 2;
			break;
		}
	}
	/*
	 * If there's no signal to deliver, we just put the saved sigmask
	 * back.
	 */
	restore_saved_sigmask();
}

위 코드는 arch_do_signal_or_restart 함수로, 시그널이 발생했다면, 시그널에 대한 정보를 인자로 get_signal 함수를 호출합니다. 해당 함수에서는 시그널에 해당하는 핸들러가 등록되어 있는지 확인합니다. 만약 핸들러가 등록되어 있다면 시그널에 대한 정보와 레지스터 정보를 인자로 handle_signal 함수를 호출합니다.


handle_signal

static void
handle_signal(struct ksignal *ksig, struct pt_regs *regs)
{
    ...
	failed = (setup_rt_frame(ksig, regs) < 0);
	if (!failed) {
		fpu__clear_user_states(fpu);
	}
	signal_setup_done(failed, ksig, stepping);
}

위 코드는 handle_signal 함수의 일부로, setup_rt_frame 함수를 호출하는 것을 볼 수 있습니다. 해당 함수는 시그널에 적용된 핸들러가 존재할 경우, 핸들러의 주소를 다음 실행 주소로 삽입합니다. 앞서 예제에서는 SIGALRM이 발생할 경우 해당 코드를 통해 sig_handler 함수가 호출됩니다.

signal handler 호출 과정

regs->si = (unsigned long)&frame->info;
regs->dx = (unsigned long)&frame->uc;
regs->ip = (unsigned long) ksig->ka.sa.sa_handler;

regs->sp = (unsigned long)frame;


sigreturn

sigreturn

현재 프로세스가 바뀌는 것을 컨텍스트 스위칭이라고 합니다. 컨텍스트 스위칭이 일어나면 다시 유저 프로세스로 복귀해야 합니다. 따라서 스위칭이 일어날 때의 상황을 커널에서 기억하고, 커널 코드의 실행을 마치면 기억한 정보를 되돌려 복귀해야합니다. 이때 사용되는 시스템 콜이 sigreturn 입니다.

아래 코드는 restore_sigcontext 함수로, sigreturn 시스템 콜을 호출하면 내부적으로 해당 함수를 호출해 스택에 저장된 값을 각각의 레지스터에 복사하여 기존 상황과 실행할 코드를 기억하고 복귀합니다.

코드를 살펴보면 sigcontext 구조체에 존재하는 각 멤버 변수에 값을 삽입하는 하는데, sigreturn 시스템 콜을 이용한 공격을 알아보기 위해 해당 구조체에 대해 알고 있어야 합니다.


sigcontext

/* __x86_64__: */
struct sigcontext {
  __u64               r8;
  __u64               r9;
  __u64               r10;
  __u64               r11;
  __u64               r12;
  __u64               r13;
  __u64               r14;
  __u64               r15;
  __u64               rdi;
  __u64               rsi;
  __u64               rbp;
  __u64               rbx;
  __u64               rdx;
  __u64               rax;
  __u64               rcx;
  __u64               rsp;
  __u64               rip;
  __u64               eflags;     /* RFLAGS */
  __u16               cs;
  __u16               gs;
  __u16               fs;
  union {
      __u16           ss; /* If UC_SIGCONTEXT_SS */
      __u16           __pad0; /* Alias name for old (!UC_SIGCONTEXT_SS) user-space */
  };
  __u64               err;
  __u64               trapno;
  __u64               oldmask;
  __u64               cr2;
  struct _fpstate __user      *fpstate;   /* Zero when no FPU context */
#  ifdef __ILP32__
  __u32               __fpstate_pad;
#  endif
  __u64               reserved1[8];
};

위 코드는 x86_64 아키텍처에 해당하는 sigcontext 구조체입니다. 따라서 다른 아키텍처의 바이너리를 공격할 때에는 대상 아키텍처를 파악하고 해당 구조체와 커널 코드를 분석해서 진행해야 합니다.



SROP

SROP

SigReturn-Oriented Programming (SROP)는 컨텍스트 스위칭을 위해 사용하는 sigreturn 시스템 콜을 이용한 ROP 기법입니다. 모든 레지스터를 조작할 수 있는 만큼 익스플로잇 활용도가 매우 높습니다.

// Name: sigrt_call.c
// Compile: gcc -o sigrt_call sigrt_call.c 

#include <string.h>
int main()
{
        char buf[1024];
        memset(buf, 0x41, sizeof(buf));
        
        asm("mov $15, %rax;"
            "syscall");
}

위 코드는 sigreturn 시스템 콜을 호출해 레지스터를 스택의 값으로 조작하는 예제 코드입니다.

$ gdb-gef -q sigrt_call
Reading symbols from sigrt_call...(no debugging symbols found)...done.
GEF for linux ready, type `gef' to start, `gef config' to configure
96 commands loaded for GDB 8.1.1 using Python engine 3.6
gef➤  r
Starting program: /home/ion/dreamhack/SROP/sigrt_call

Program received signal SIGSEGV, Segmentation fault.
[ Legend: Modified register | Code | Heap | Stack | String ]
───────────────────────────────────────────────────────────────────────────────────────────────────────── registers ────
$rax   : 0x0
$rbx   : 0x4141414141414141 ("AAAAAAAA"?)
$rcx   : 0x4141414141414141 ("AAAAAAAA"?)
$rdx   : 0x4141414141414141 ("AAAAAAAA"?)
$rsp   : 0x4141414141414141 ("AAAAAAAA"?)
$rbp   : 0x4141414141414141 ("AAAAAAAA"?)
$rsi   : 0x4141414141414141 ("AAAAAAAA"?)
$rdi   : 0x4141414141414141 ("AAAAAAAA"?)
$rip   : 0x4141414141414141 ("AAAAAAAA"?)
$r8    : 0x4141414141414141 ("AAAAAAAA"?)
$r9    : 0x4141414141414141 ("AAAAAAAA"?)
$r10   : 0x4141414141414141 ("AAAAAAAA"?)
$r11   : 0x4141414141414141 ("AAAAAAAA"?)
$r12   : 0x4141414141414141 ("AAAAAAAA"?)
$r13   : 0x4141414141414141 ("AAAAAAAA"?)
$r14   : 0x4141414141414141 ("AAAAAAAA"?)
$r15   : 0x4141414141414141 ("AAAAAAAA"?)
$eflags: [ZERO CARRY parity adjust sign TRAP INTERRUPT direction overflow RESUME virtualx86 identification]

0개의 댓글