SROP 는 sigreturn 시스템 콜을 이용하여 레지스터에 원하는 값을 저장할 수 있다.
- 해당 기법을 이용하여 원하는 시스템 함수를 호출할 수 있다.
Signal
- signal은 프로세스에게 이벤트가 발생했음을 알린다.
- signal은 다른 프로세스에게 시그널을 전송 할 수 있다.
- signal은 원시적인 형태의 IPC(interprocess communication) 로 사용 할 수 있다.
- signal은 자기 자신에게 시그널을 보낼수도 있다.
- signal은 일반적으로 커널이 송신하며, 다음과 같은 이벤트 종류가 있다.
- 하드웨어 예외가 발생한 경우
- 사용자가 시그널을 발생시키는 터미널 특수 문자 중 하나를 입력한 경우
- Interrupt character ( Control + c)
- Suspend character ( Control + z)
- 소프트웨어 이벤트가 발생한 경우
- 파일 디스크립터에 입력이 발생
- 타이머 만료
- 해당 프로세스의 자식 프로세스가 종료
- signal은 생성되면 프로세스에 전달되고, 전달된 시그널의 종류에 따라 다음과 같은 동작이 실행된다.
- 시그널 무시한다.
- 프로세스를 종료한다.
- 코어 덤프 파일을 생성 후 프로세스를 종료한다.
- 프로세스를 중지한다.
- 프로세스의 실행을 재개한다.
Signal handler
- signal handler는 프로그램이 특정 시그널의 기본 동작을 수행하는 대신 프로그래머가 원하는 동작을 수행하도록 변경할 수 있다.
- signal handler는 User Mode 프로세스에 정의되어 있고 User Mode 코드 세그먼트에 포함된다.
- signal handler가 User Mode 에서 실행되는 동안 Kernel Mode 에서 handle_signal() 함수가 실행된다.
- User Mode 에서 Kernel Mode로 진입시 User Mode 에서 사용중이던 context를 Kernel stack 에 저장한다.
- Kernel Mode 에서 User Mode 로 진입시 Kernel stack 은 모두 초기화된다.
- 이러한 문제를 해결하기 위해 setup_frame(), sigreturn() 함수를 사용한다.
- setup_frame() : User Mode 의 stack을 설정
- sigreturn(): Kernel Mode Stack 에 hardware context를 복사하고, User Mode Stack의 원래 context를 저장한다.
- Signal handler는 다음과 같이 처리된다.
- 인터럽트 또는 예외가 발생하면 프로세스는 Kernel Mode 로 전환된다.
- 커널은 User Mode로 돌아가기 전에
do_signal()
함수를 실행한다.do_signal()
함수는handle_signal()
을 호출하여 signal을 처리한다.handle_signal()
함수는setup_frame()
을 호출하여 User Mode Stack 에 context를 저장한다.
- 프로세스가 User Mode로 다시 전환되면 signal handler가 실행된다.
- signal handler가 종료되면
setup_frame()
함수에 의해 User Mode stack에 저장된 리턴 코드가 실행된다.- 해당 코드에 의해
sigreturn()
시스템 함수가 호출된다.sigreturn()
시스템 함수에 의해 Kernel Mode Stack 에서 일반 프로그램의 hardware context를 User Mode 의 stack에 복사한다.sigreturn()
함수는restore_sigcontext()
를 호출하여 User Mode 스택을 원래 상태로 복원한다.
- 해당 코드에 의해
- 시스템 호출이 종료되면 일반 프로그램은 실행을 재개할 수 없다.
[Kernel] Process Context
process context 는 프로세스가 현재 어떤 상태에서 수행되고 있는지 정확히 규명하기 위해 필요한 정보를 말한다.
process context 는 3가지로 하드웨어 문맥
, 프로세스의 주소공간
, 커널상의 문맥
으로 나눌 수 있다.
하드웨어 문맥 : CPU 수행 상태를 나타내는 것으로 PC(Program Counter) 와 각종 레지스터에 저장하고 있는 값들을 말한다.
프로세스의 주소공간: 코드, 데이터, 스택 으로 구성된 프로세스만의 독자적인 주소 공간을 말한다.
커널상의 문맥: 프로세스 관리를 위한 자료구조인 PCB(Process Control Block)와 Kernel stack(커널내의 주소) 를 말한다.
PCB(Process Contorl Block) 이란
- 운영체제가 시스템 내의 프로세스들을 관리하기 위해 프로세스마다 유지하는 정보들을 담는 커널 내의 자료구조를 말한다.
Sigcontext
[User Mods] User Process →
[Kernel Mods] Save sigcontext →
do_signal()
→handle_signal()
→setup_frame()
- User Mode Stack 에 context 저장
[User Mods] signal handler →
[Kernel Mods] Restore sigcontext →
system_call()
: User Mode Stack에 저장된 리턴 코드가 실행 →sys_sigreturn()
호출sys_sigreturn()
: Kernel Mode Stack 에서 일반 프로그램의 hardware context를 User Mode의 stack에 복사 후restore_sigcontext()
호출restore_sigcontext()
: User Mode 스택을 원래 상태로 복원
Sigreturn()
sigreturn()
함수는 Signal을 처리하는 프로세스가 Kernel Mode에서 User Mode로 돌아올 때 Stack을 복원하기 위해 사용하는 함수이다.
실습 ( lazenca.net sigreturn 실습)
//gcc -m32 -g -o sig32 sig.c
#include <stdio.h>
#include <signal.h>
struct sigcontext sigcontext;
void handle_signal(int signum){
printf("Signal number: %d\n", signum);
}
int main(){
signal(SIGINT, (void *)handle_signal);
while(1) {}
return 0;
}
handel_signal
에 브레이크를 걸고
GDB가 인터럽트에 반응하지 않도록 설정
프로그램을 실행한 후 “Ctrl + C” 로 인터럽트 발생
bt
명령어로 handle_signal
함수가 호출되기 전에 실행된 함수 목록 확인
0번째 Frame 에서 Stack 에 저장된 각각의 레지스터 값을 확인
p/x ((struct sigcontext *)($ebp+3*4)) -> eax
p/x ((struct sigcontext *)($ebp+3*4)) -> esp
p/x ((struct sigcontext *)($ebp+3*4)) -> eip
1번째 Frame의 내용 __kernel_sigreturn()
함수에서 sys_sigreturn()
시스템함수를 호출한다.
- x86에서
sys_sigreturn
시스템 함수의 번호는 0x77이다.
signal에 대한 처리가 끝난 후에 Frame 0 의 Stack에 저장된 값이 레지스터에 저장된 것을 확인할 수 있다.
sigreturn()
함수는 stack을 복원하기 위해restroe_sigcontext()
를 호출한다.- SYS_sigreturn 을 호출 (0x77)
#ifdef CONFIG_X86_32
asmlinkage unsigned long sys_sigreturn(void){
struct pt_regs *regs = current_pt_regs();
struct sigframe __user *frame;
...
if (restore_sigcontext(regs, &frame->sc, 0))
goto badframe;
...
}
#endif /* CONFIG_X86_32 */
#ifdef CONFIG_X86_32
SYSCALL_DEFINE0(sigreturn)
{
struct pt_regs *regs = current_pt_regs();
struct sigframe __user *frame;
sigset_t set;
frame = (struct sigframe __user *)(regs->sp - 8);
if (!access_ok(frame, sizeof(*frame)))
goto badframe;
if (__get_user(set.sig[0], &frame->sc.oldmask) ||
__get_user(set.sig[1], &frame->extramask[0]))
goto badframe;
set_current_blocked(&set);
/*
* x86_32 has no uc_flags bits relevant to restore_sigcontext.
* Save a few cycles by skipping the __get_user.
*/
if (restore_sigcontext(regs, &frame->sc, 0))
goto badframe;
return regs->ax;
badframe:
signal_fault(regs, frame, "sigreturn");
return 0;
}
arch/x86/kernel/signal.c
static int 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 -EFAULT;
#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));
}
arch/x86/kernel/signal.c
SYSCALL_DEFINE0
0은 인수를 나타내는 것으로 0개의 인수를 취하는 함수란 뜻이다.
sigreturn() 함수는 stack을 복원하기 위해 restore_sigcontext()
를 호출한다.
static int 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 -EFAULT;
#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));
}
kernel 5.7 이전에는 COPY_SEG(), COPY() 를 이용해서 복사했음.
static int restore_sigcontext(struct pt_regs *regs,
struct sigcontext __user *sc,
unsigned long uc_flags)
{
unsigned long buf_val;
void __user *buf;
unsigned int tmpflags;
unsigned int err = 0;
/* Always make any pending restarted system calls return -EINTR */
current->restart_block.fn = do_no_restart_syscall;
get_user_try {
#ifdef CONFIG_X86_32
set_user_gs(regs, GET_SEG(gs));
COPY_SEG(fs);
COPY_SEG(es);
COPY_SEG(ds);
#endif /* CONFIG_X86_32 */
COPY(di); COPY(si); COPY(bp); COPY(sp); COPY(bx);
COPY(dx); COPY(cx); COPY(ip); COPY(ax);
#ifdef CONFIG_X86_64
COPY(r8);
COPY(r9);
COPY(r10);
COPY(r11);
COPY(r12);
COPY(r13);
COPY(r14);
COPY(r15);
#endif /* CONFIG_X86_64 */
COPY_SEG_CPL3(cs);
COPY_SEG_CPL3(ss);
get_user_ex(tmpflags, &sc->flags);
regs->flags = (regs->flags & ~FIX_EFLAGS) | (tmpflags & FIX_EFLAGS);
regs->orig_ax = -1; /* disable syscall checks */
get_user_ex(buf_val, &sc->fpstate);
buf = (void __user *)buf_val;
} get_user_catch(err);
#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
err |= fpu__restore_sig(buf, IS_ENABLED(CONFIG_X86_32));
return err;
}
arch/x86/kernel/signal.c
#define COPY(x) do { \
get_user_ex(regs->x, &sc->x); \
} while (0)
#define GET_SEG(seg) ({ \
unsigned short tmp; \
get_user_ex(tmp, &sc->seg); \
tmp; \
})
#define COPY_SEG(seg) do { \
regs->seg = GET_SEG(seg); \
} while (0)
arch/x86/kernel/signal.c
#define get_user_ex(x, ptr) do { \
unsigned long __gue_val; \
__get_user_size_ex((__gue_val), (ptr), (sizeof(*(ptr)))); \
(x) = (__force __typeof__(*(ptr)))__gue_val; \
} while (0)
arch/x86/include/asm/uaccess.h
stack에 저장된 값을 각 레지스터에 복사한다.
- 즉, ROP와 같이 값을 레지스터에 저장할 수 있는 Gadget 없이도 sigreturn() 함수를 이용해 각 레지스터에 원하는 것을 저장할 수 있다.
stack에 저장된 값들은 restore_sigcontext()함수의 인자값 sc에 전달된다. <kernel v1.6.19 이하 버전에서>
struct sigcontext sc;
로 선언해줌.
# ifdef __i386__
struct sigcontext {
__u16 gs, __gsh;
__u16 fs, __fsh;
__u16 es, __esh;
__u16 ds, __dsh;
__u32 edi;
__u32 esi;
__u32 ebp;
__u32 esp;
__u32 ebx;
__u32 edx;
__u32 ecx;
__u32 eax;
__u32 trapno;
__u32 err;
__u32 eip;
__u16 cs, __csh;
__u32 eflags;
__u32 esp_at_signal;
__u16 ss, __ssh;
struct _fpstate __user *fpstate;
__u32 oldmask;
__u32 cr2;
};
SROP를 할 때 sigcontext 구조체의 형태로 저장해야한다.
SROP Exploit Method
-
sigreturn() 함수를 이용해 레지스터에 필요한 값을 저장
- ESP: sigreturn() 함수 호출 후 이동할 주소(’int 0x80” 명령어가 저장된 주소)
- EBX: “/bin/sh” 문자열이 저장된 주소
- EAX: execve() 함수의 시스템 콜 번호
- EIP: “int 0x80” 명령어가 저장된 주소
- CS: User Code (0x23)
- SS: User Data / Stack (0x2b)
-
int 0x80 명령어 실행
sigreturn()
int 0x80
- 공격을 위해 알아내야 할 정보는 다음과 같다.
- Libc offset
- Printf
- __kernel_sigreturn
- “/bin/sh” 명령이 저장된 영역
- Gadgets
- int 0x80
- Libc offset
SROP 공격에 CS SS 조작이 필요한 이유
sigcontext 구조체 형태로 stack에 값을 저장할 때 최소한 CS, SS 레지스터에 대한 값을 설정해야한다.
- Linux kernel 에는 4개의 세그먼트만 존재한다.
- 공격코드들은 User Mode 에서 실행되기 때문에 User Code, User Data / Stack 값을 사용해야 한다.
- 32bit 프로그램의 경우 실행되는 운영체제 (32bit/ 64bit) 환경에 따라 사용되는 세그먼트 값이 다르다.
- 32bit 운영체제에서는 0x73, 0x7b 가 사용된다.
- 62bit 운영체제에서는 실행되는 32bit 프로그램의 경우 0x23, 0x2b가 사용된다.
- 이외의 값을 저장하게 되면 에러가 발생한다.
purpose | segment(32bit) | segment(64bit-32bit) |
kernel Code | 0x60 | 0x8 |
kernel Data/ Stack | 0x68 | 0x18 |
User Code | 0x73 | 0x23 |
User Data/ Stack | 0x7b | 0x2b |
dreamhack srop 실습
// Name: srop.c
// Compile: gcc -o srop srop.c -fno-stack-protector -no-pie
#include <unistd.h>
int gadget() {
asm("pop %rax;"
"syscall;"
"ret" );
}
int main()
{
char buf[16];
read(0, buf ,1024);
}
buf
는 16바이트만큼 할당 0x10 바이트
buf
1024 바이트 만큼 읽을 수 있음
int gadget() {
asm("pop %rax;"
"syscall;"
"ret" );
}
gadget
함수에서 syscall 을 한다.
gadget
함수를 통해서 sigreturn 시스템 콜을 호출해 레지스터를 조작하고 셸을 획득할 수 있다.
이 글은 옵시디언을 이용해서 작성되었습니다.
'TOOR' 카테고리의 다른 글
[TOOR] 14. RELRO (0) | 2023.09.29 |
---|---|
[TOOR] 13.1. PIE (0) | 2023.09.29 |
[TOOR] 12.2 rop_2 write_up (0) | 2023.09.24 |
[TOOR] 12.1. ROP & ropasaurusrex write_up (0) | 2023.09.24 |
[TOOR] 11.2. One (Shot) gadget (0) | 2023.09.24 |