SigReturn-Oriented Programming

dandb3·2024년 4월 10일
0

pwnable

목록 보기
17/22
post-thumbnail

signal

리눅스에서는 signal이 발생했을 경우, 해당 signal에 대한 handler 함수가 존재한다면 해당 함수로 context가 바뀌고, 그 handler 함수가 종료되고 나면 다시 원래 context로 돌아오게 된다.

context switch

user mode에서 kernel mode로의 전환 시, TCB(Task Control Block)에 user mode에서의 context가 저장된다.
그리고 kernel mode에서 작업이 다 끝난 후에 user mode로 돌아갈 때, 저장되어 있던 TCB의 내용을 바탕으로 다시 원래 context를 복원하게 된다.

이러한 특성 때문에 그냥 단순한 TCB의 도움으로는 signal handler의 호출을 하기 어려움.

이유

  1. 처음 signal 발생 시 kernel mode로 전환되고, 그 후에 원래 context가 아닌 signal handler 함수로의 context로 바뀌어야 한다.
  2. signal handler 함수가 실행된 후에 다시 원래 context로 돌아가야 하는데, signal handler 함수 종료 후 kernel mode로 진입하게 되면 TCB에는 signal handler의 context가 저장되기 때문에 기존 context의 내용은 사라지게 된다.

이를 해결하는 방법은 다음과 같다.

(출처 - https://pwnkidh8n.tistory.com/197?category=896828)

첫 번째 kernel mode에서는 원래 context의 내용을 user stack에 저장해 놓는다.
두 번째 kernel mode에서는 user stack에 저장되어 있던 원래 context를 복원한다.

이런 방식으로 user stack을 사용하게 된다.

첫 번째 kernel mode

arch_do_signal_or_restart()

void arch_do_signal_or_restart(struct pt_regs *regs)
{
	struct ksignal ksig;

	if (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) != -1) {
    ...
}

먼저 arch_do_signal_or_restart()가 실행된다.
자세하게는 모르겠지만, signal이나 system call(아닐 가능성 높음)을 처리하는데에 사용되는 함수인듯..?

함수를 잘 보면, get_signal()을 통해서 수신한 시그널에 대한 정보를 가져오고, 만약 시그널이 맞다면 handle_signal()을 호출한다는 것을 알 수 있다.

get_signal()

bool get_signal(struct ksignal *ksig)
{
	...
    return ksig->sig > 0;
}

실제로 signal을 수신했다면 true를 리턴하는 함수이다.

handle_signal()

static void
handle_signal(struct ksignal *ksig, struct pt_regs *regs)
{
	...
    failed = (setup_rt_frame(ksig, regs) < 0);
    ...
}

setup_rt_frame()을 호출하는 것을 알 수 있다.

setup_rt_frame()

static int
setup_rt_frame(struct ksignal *ksig, struct pt_regs *regs)
{
	/* Perform fixup for the pre-signal frame. */
	rseq_signal_deliver(ksig, regs);

	/* Set up the stack frame */
	if (is_ia32_frame(ksig)) {
		if (ksig->ka.sa.sa_flags & SA_SIGINFO)
			return ia32_setup_rt_frame(ksig, regs);
		else
			return ia32_setup_frame(ksig, regs);
	} else if (is_x32_frame(ksig)) {
		return x32_setup_rt_frame(ksig, regs);
	} else {
		return x64_setup_rt_frame(ksig, regs);
	}
}

우리는 x86-64에 대해서 분석하고 있으니 x64_setup_rt_frame()으로 넘어가자.

x64_setup_rt_frame()

int x64_setup_rt_frame(struct ksignal *ksig, struct pt_regs *regs)
{
	sigset_t *set = sigmask_to_save();
	struct rt_sigframe __user *frame;
	void __user *fp = NULL;
	unsigned long uc_flags;

	/* x86-64 should always use SA_RESTORER. */
	if (!(ksig->ka.sa.sa_flags & SA_RESTORER))
		return -EFAULT;

	frame = get_sigframe(ksig, regs, sizeof(struct rt_sigframe), &fp);
	uc_flags = frame_uc_flags(regs);

	if (!user_access_begin(frame, sizeof(*frame)))
		return -EFAULT;

	/* Create the ucontext.  */
	unsafe_put_user(uc_flags, &frame->uc.uc_flags, Efault);
	unsafe_put_user(0, &frame->uc.uc_link, Efault);
	unsafe_save_altstack(&frame->uc.uc_stack, regs->sp, Efault);

	/* Set up to return from userspace.  If provided, use a stub
	   already in userspace.  */
	unsafe_put_user(ksig->ka.sa.sa_restorer, &frame->pretcode, Efault);
	unsafe_put_sigcontext(&frame->uc.uc_mcontext, fp, regs, set, Efault);
	unsafe_put_sigmask(set, frame, Efault);
	user_access_end();

	...
}

여기서 __user는 내 생각에는, 코드 상에서는 의미가 없지만, user mode와 관련된 변수의 경우 앞에 붙여주는 키워드인 것 같다. (뇌피셜)

위 함수는 실제 signal handler 함수가 실행되기 전에 여러 사전작업을 해 주는 함수이다.
frame 의 값이 바로 실제 스택에서의 context에 대한 정보가 저장되는 메모리 주소이다.

frame은 어떻게 생긴 녀석인지 확인해 보자.

struct rt_sigframe

struct rt_sigframe
{
	char __user *pretcode;
	struct ucontext uc;
	struct siginfo info;
	struct _xstate fpstate;
};

여기서 자세히 확인해 볼 부분은 struct ucontext uc; 부분이다.

struct ucontext

struct ucontext {
	unsigned long	  uc_flags;
	struct ucontext  *uc_link;
	stack_t		  uc_stack;
	struct sigcontext uc_mcontext;
	sigset_t	  uc_sigmask;	/* mask last for extensibility */
};

uc_mcontext가 바로 우리가 찾던 sigreturn 시에 되돌릴 context가 저장되어 있는 부분이다.

stack_t

typedef struct sigaltstack {
	void __user *ss_sp;
	int ss_flags;
	__kernel_size_t ss_size;
} stack_t;

메모리 주소 계산용으로 추가함.

struct sigcontext

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

딱 생긴 것만 봐도 알겠지만, 이 구조체에 저장된 값이 sys_rt_sigreturn()을 통해서 원래 context를 복구하는 데에 쓰인다.

이런 흐름을 통해서 원래 context를 user stack에 저장하게 된다.
그 다음에 아래 부분을 보자.

int x64_setup_rt_frame(struct ksignal *ksig, struct pt_regs *regs)
{
	...
    
	/* Set up registers for signal handler */
	regs->di = ksig->sig;
	/* In case the signal handler was declared without prototypes */
	regs->ax = 0;

	/* This also works for non SA_SIGINFO handlers because they expect the
	   next argument after the signal number on the stack. */
	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;
    
    ...
}

여기서 handler 함수 호출 전에 여러 레지스터 값들을 설정해 주는 것을 확인할 수 있다.
ip는 handler의 주소를 가리키고, sp는 frame의 주소, di는 signal값이 저장된다는 사실 정도만 기억해 두면 좋을 것 같다.

두 번째 kernel mode

두 번째는 sys_rt_sigreturn syscall을 이용한다.
예제 코드와 같이 설명할 것이다.

#include <signal.h>
#include <stdio.h>
#include <unistd.h>

void sigHandler(int sig)
{
    printf("Hello World!\n");
}

int main()
{
    signal(SIGINT, sigHandler);
    kill(getpid(), SIGINT);
    return 0;
}

사진을 보면, sigHandler 호출 이후에 __restore_rt()로 넘어가는 부분을 확인할 수 있는데, 이 함수는 단순히 0xf번 syscall을 호출해 주는데, 이 syscall이 바로 sigreturn에 해당한다.

그 다음에 stack의 값을 확인해 볼 건데, 그 전에 x64_setup_rt_frame()에서의 동작을 다시 보자.

int x64_setup_rt_frame(struct ksignal *ksig, struct pt_regs *regs)
{
	...
    
	/* Set up registers for signal handler */
	regs->di = ksig->sig;
	/* In case the signal handler was declared without prototypes */
	regs->ax = 0;

	/* This also works for non SA_SIGINFO handlers because they expect the
	   next argument after the signal number on the stack. */
	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;
    
    ...
}

handler를 호출하기 전에 sp의 값을 frame 값으로 설정하는 것을 확인할 수 있다.

handler함수 리턴 직후와, 원래 context로 돌아갔을 때의 상황을 보자.


원래 context에서의 r8값은 0x7ffff3d5b0d0에 해당한다.
handler함수 리턴 직후의 메모리 상태를 보았을 때 0x7ffff3d5b0d0의 값은 $rsp + 0x28에 위치해 있으며, 물론 그 이후에 r9, r10, r11, ... 의 값들이 메모리 상에 연속적으로 존재함을 확인할 수 있다.

원래는 소스코드를 보고 메모리 offset을 직접 계산해 보려고 했는데, 뭔가 안 맞음. 내가 해 보면 0x30이 나오는데, 다른 아키텍쳐의 소스코드를 잘못 본 것인지, 뭔지는 모르겠음. 어쨌든 확실한거는 struct sigcontext 이전의 frame의 멤버변수들의 크기가 0x28이라는거.

추가

+--------------------+--------------------+
| rt_sigeturn()      | uc_flags           |
+--------------------+--------------------+
| &uc                | uc_stack.ss_sp     |
+--------------------+--------------------+
| uc_stack.ss_flags  | uc.stack.ss_size   |
+--------------------+--------------------+
| r8                 | r9                 |
+--------------------+--------------------+
| r10                | r11                |
+--------------------+--------------------+
| r12                | r13                |
+--------------------+--------------------+
| r14                | r15                |
+--------------------+--------------------+
| rdi                | rsi                |
+--------------------+--------------------+
| rbp                | rbx                |
+--------------------+--------------------+
| rdx                | rax                |
+--------------------+--------------------+
| rcx                | rsp                |
+--------------------+--------------------+
| rip                | eflags             |
+--------------------+--------------------+
| cs / gs / fs       | err                |
+--------------------+--------------------+
| trapno             | oldmask (unused)   |
+--------------------+--------------------+
| cr2 (segfault addr)| &fpstate           |
+--------------------+--------------------+
| __reserved         | sigmask            |
+--------------------+--------------------+

sigcontext 구조체는 위와 같이 생겼다.
즉, 첫 8바이트는 사실 __restore_rt()함수의 주소를 가리키는 것이다.
그러므로, 0x30의 offset을 가지는 것이 맞다.

exploit의 관점

되게 장황하게 알아보았지만, 결국 우리가 이용하고자 하는 것은 바로 sigreturn syscall에 해당한다.
struct sigcontext에 해당하는 부분의 메모리를 내가 원하는 context로 조작하고, sigreturn syscall을 호출하게 된다면 원하는 흐름으로 바꿀 수 있다.

물론, $rsp + 0x28의 위치라는 사실을 잊으면 안 된다.

추가로 궁금한 점

사실 cs레지스터, ss레지스터의 값이 0x33, 0x2b인 이유.. 에 대해서는 잘 모르겠다.
user mode에서의 값이 0x33, 0x2b이고
kernel mode에서의 값이 0x10, 0x18인 것 까지는 어떻게 알겠는데.. 이 값이 가지는 의미? 에 대해서는 좀 더 공부가 필요할 듯 하다.

참고자료

https://elixir.bootlin.com/linux/latest/source
https://pwnkidh8n.tistory.com/197?category=896828
https://pwnkidh8n.tistory.com/200
https://book.hacktricks.xyz/binary-exploitation/rop-return-oriented-programing/srop-sigreturn-oriented-programming

profile
공부 내용 저장소

0개의 댓글