CIDY
[System_Hacking] AD: stage5_SigReturn Oriented Programming 본문
[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시스템 콜을 호출해 레지스터를 스택 값으로 조작하는 코드이다.
위 코드를 실행시켜 보면, 이렇게 스택에 든 값으로 레지스터가 모두 조작된 것을 확인할 수 있다.
'Hack > DreamHack(로드맵)' 카테고리의 다른 글
[System_Hacking] AD: stage5_문제풀이(send_sig) (0) | 2022.07.14 |
---|---|
[System_Hacking] AD: stage5_문제풀이(SigReturn-Oriented Programming) (0) | 2022.07.14 |
[System_Hacking] AD: stage4_문제풀이(rtld) (0) | 2022.07.14 |
[System_Hacking] AD: stage4_문제풀이(__environ) (0) | 2022.07.14 |
[System_Hacking] AD: stage4_문제풀이(Overwrite _rtld_global) (0) | 2022.07.14 |