Recent Posts
Recent Comments
Link
«   2024/11   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
Tags
more
Archives
Today
Total
관리 메뉴

CIDY

[System_Hacking] AD: stage5_SigReturn Oriented Programming 본문

Hack/DreamHack(로드맵)

[System_Hacking] AD: stage5_SigReturn Oriented Programming

CIDY 2022. 7. 14. 05:49

*Signal

예전에 syscall에 대해 설명하면서 (Ck수업) 운영체제는 유저모드와 커널 모드로 나뉜다고 한 적이 있다. 프로그램을 실행하는 과정은 이 두 모드가 상호작용하며 이루어진다. 

 

시그널은 프로세스에 특정 정보를 전달하는 매개체인데, 지겹게 보던 그 SIGSEGV같은것도 시그널의 일종이다. 리눅스에서는 다양한 시그널을 제공하는데,

 

 *	+--------------------+------------------+
 *	|  POSIX signal      |  default action  |
 *	+--------------------+------------------+
 *	|  SIGHUP            |  terminate	|
 *	|  SIGINT            |	terminate	|
 *	|  SIGQUIT           |	coredump 	|
 *	|  SIGILL            |	coredump 	|
 *	|  SIGTRAP           |	coredump 	|
 *	|  SIGABRT/SIGIOT    |	coredump 	|
 *	|  SIGBUS            |	coredump 	|
 *	|  SIGFPE            |	coredump 	|
 *	|  SIGKILL           |	terminate(+)	|
 *	|  SIGUSR1           |	terminate	|
 *	|  SIGSEGV           |	coredump 	|
 *	|  SIGUSR2           |	terminate	|
 *	|  SIGPIPE           |	terminate	|
 *	|  SIGALRM           |	terminate	|
 *	|  SIGTERM           |	terminate	|
 *	|  SIGCHLD           |	ignore   	|
 *	|  SIGCONT           |	ignore(*)	|
 *	|  SIGSTOP           |	stop(*)(+)  	|
 *	|  SIGTSTP           |	stop(*)  	|
 *	|  SIGTTIN           |	stop(*)  	|
 *	|  SIGTTOU           |	stop(*)  	|
 *	|  SIGURG            |	ignore   	|
 *	|  SIGXCPU           |	coredump 	|
 *	|  SIGXFSZ           |	coredump 	|
 *	|  SIGVTALRM         |	terminate	|
 *	|  SIGPROF           |	terminate	|
 *	|  SIGPOLL/SIGIO     |	terminate	|
 *	|  SIGSYS/SIGUNUSED  |	coredump 	|
 *	|  SIGSTKFLT         |	terminate	|
 *	|  SIGWINCH          |	ignore   	|
 *	|  SIGPWR            |	terminate	|
 *	|  SIGRTMIN-SIGRTMAX |	terminate       |
 *	+--------------------+------------------+
 *	|  non-POSIX signal  |  default action  |
 *	+--------------------+------------------+
 *	|  SIGEMT            |  coredump	|
 *	+--------------------+------------------+

 

리눅스 소스코드에 정의된 시그널은 위와 같다. 

 

시그널이 발생할 시, 시그널에 해당하는 코드가 커널 모드에서 실행되고 다시 유저 모드로 복귀하는 과정을 거친다.

 

#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;
}

 

문제를 풀면 정상 종료를 위해 alarm함수를 설정해 둔 것을 종종 볼 수 있다.  

 

위 코드는 signal함수를 이용하는데, 저게 무슨 뜻이냐면 SIGALRM 시그널 발생 시 sig_haldler함수를 실행하겠다는 뜻이다. -> 프로세스에서 다 처리하는 게 아니라, SIGALRM시그널이 발생하면 커널 모드로 진입한다.

 

여기서, 시그널을 커널 모드에서 처리하고 다면 다시 유저 모드로 그대로 돌아와 프로세스의 코드를 실행해야 한다. -> 어떻게 유저 모드 상태를 고스란히 기억하고 시그널이 발생했을 당시의 메모리 상태나 레지스터 세팅으로 정확히 돌아올 수 있을까? 

 

물론 내가 걱정 안 해도 알아서 잘 세팅돼 있겠지만.. 커널에서는 유저모드로 돌아가는 상황을 고려해 유저 모드 프로세스 상태를 저장하는 코드가 구현되어 있다.

 

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();
}

 

do_signal함수는 시그널 처리를 위해 가장 먼저 호출되는 친구이다. 

 

참고로 리눅스 커널 5.8버전 이하에서는 do_signal이고, 5.10이하 버전에서는 arch_go_signal이고, 그보다 더 상위 버전에서는 arch_do_signal_or_restart라는 이름을 갖고 있다.

 

암튼 그래서 위 코드는 arch_do_signal_or_restart함수인데, 시그널 발생 시 시그널에 대한 정보(&ksig)를 인자로 get_signal함수를 호출한다. -> 이 함수에서는 시그널에 해당하는 핸들러가 등록돼있는지 확인함 -> 등록되어 있을 시, 시그널에 대한 정보와 레지스터(regs) 정보를 인자로 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함수를 호출하는데, -> 그 함수는 뭘 하냐면, 

 

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;

 

시그널에 적용된 핸들러가 있다면 위와 같은 과정으로 핸들러의 주소를 다음 실행 주소로 삽입한다.

 

 

다시 원래 예제 C코드로 돌아가 보자면, SIGALRM이 발생할 경우 sig_handler가 호출되었었다. 

 

 

 

*sigreturn

현재 프로세스가 바뀌는 것을 Context Switching이라고 한다. 위에서 봤듯이, 커널은 유저가 실행한 프로세스를 관리하기 위해 다양한 코드를 실행하는데, 컨텍스트 스위칭이 발생하면 다시 유저모드로 복귀해댜 한다 -> 그리고 스위칭이 일어날 때의 상황을 커널에서 기억하고 -> 커널 코드의 실행을 마치면 기억한 정보를 토대로 살포시 그대로 복귀해야 한다. -> 이때 사용되는 시스템 콜이 sigreturn이다.

 

static bool restore_sigcontext(struct pt_regs *regs,
			       struct sigcontext __user *usc,
			       unsigned long uc_flags)
{
	struct sigcontext sc;
	/* Always make any pending restarted system calls return -EINTR */
	current->restart_block.fn = do_no_restart_syscall;
	if (copy_from_user(&sc, usc, CONTEXT_COPY_SIZE))
		return false;
#ifdef CONFIG_X86_32
	set_user_gs(regs, sc.gs);
	regs->fs = sc.fs;
	regs->es = sc.es;
	regs->ds = sc.ds;
#endif /* CONFIG_X86_32 */
	regs->bx = sc.bx;
	regs->cx = sc.cx;
	regs->dx = sc.dx;
	regs->si = sc.si;
	regs->di = sc.di;
	regs->bp = sc.bp;
	regs->ax = sc.ax;
	regs->sp = sc.sp;
	regs->ip = sc.ip;
#ifdef CONFIG_X86_64
	regs->r8 = sc.r8;
	regs->r9 = sc.r9;
	regs->r10 = sc.r10;
	regs->r11 = sc.r11;
	regs->r12 = sc.r12;
	regs->r13 = sc.r13;
	regs->r14 = sc.r14;
	regs->r15 = sc.r15;
#endif /* CONFIG_X86_64 */
	/* Get CS/SS and force CPL3 */
	regs->cs = sc.cs | 0x03;
	regs->ss = sc.ss | 0x03;
	regs->flags = (regs->flags & ~FIX_EFLAGS) | (sc.flags & FIX_EFLAGS);
	/* disable syscall checks */
	regs->orig_ax = -1;
#ifdef CONFIG_X86_64
	/*
	 * Fix up SS if needed for the benefit of old DOSEMU and
	 * CRIU.
	 */
	if (unlikely(!(uc_flags & UC_STRICT_RESTORE_SS) && user_64bit_mode(regs)))
		force_valid_ss(regs);
#endif
	return fpu__restore_sig((void __user *)sc.fpstate,
			       IS_ENABLED(CONFIG_X86_32));
}

 

이건 restore_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];
};

 

이게 뭐냐면 그 구조체다. (sigcontext) 레지스터의 이름과 같은 이름을 가진 변수들이 멤버로 선언되어 있다. (이건 x86_64기준)

 

암튼 이런 식으로 관리되기 때문에 여기서 레지스터를 조작할 수 있다.

 

 

*SROP

SigReturn-Oriented Programming의 약자이다. SROP는 컨텍스트 스위칭을 이용하는 sigreturn 시스템 콜을 이용한 ROP이다. ROP는 예전에 많이 했었는데, 여기저기서 가젯 데려와서 필요한 요소들을 이어붙이고 실행흐름을 원하는 방향으로 조작해 쉘을 얻어내는 공격 기법이었다. -> SROP는 sigreturn시스템 콜을 호출하고, 레지스터에 복사할 값을 미리 스택에 저장해 임의의 코드를 실행하도록 하는 것이다.

 

위 구조체 멤버들을 보면 알겠지만, 모든 레지스터를 조작할 수 있어 꽤 유용하다.

 

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

 

이 코드는 sigreturn시스템 콜을 호출해 레지스터를 스택 값으로 조작하는 코드이다. 

 

 

위 코드를 실행시켜 보면, 이렇게 스택에 든 값으로 레지스터가 모두 조작된 것을 확인할 수 있다.