보호기법과 우회방법

보호기법과 우회방법

2024년 6월 3일

Tools #

checksec #

RELRO, Canary, NX, PIE 보호기법을 파악할 수 있는 리눅스 도구이며, pwntools에 포함되어 있고 ~/.local/bin/checksec에 위치한다.

b1f34516-f7c1-41fc-8c0b-d60ed24e67c1


Stack Canary #

개요 #

스택의 overflow 를 통해 return address를 조작할 수 없도록 return address 앞에 랜덤한 값을 삽입하고, 함수의 에필로그에서 값의 변조를 확인하는 기법이다.
컴파일러의 구현에 따라 여러 종류의 카나리가 있을 수 있으며 꼭 rbp 앞에 생기는 것은 아니기 때문에 바이너리를 확인해야 한다.

BOF가 발생하는 경우 잘못된 리턴주소 접근으로 인해 Segmentation Fault가 발생하지만, Canary에서 조작이 발견된 경우 stack smashing detected, Aborted가 발생한다.

 1$ gcc -o no_canary canary.c -fno-stack-protector
 2$ ./no_canary
 3HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH
 4Segmentation fault
 5
 6$ gcc -o canary canary.c
 7$ ./canary
 8HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH
 9*** stack smashing detected ***: <unknown> terminated
10Aborted

리눅스에서 fs 레지스터는 TLS(Thread Local Storage) 영역을 가리키는데 (윈도우는 32:fs / 64:gs), fs:0x28 위치에는 프로세스를 시작할 때 스레드마다 랜덤한 값을 저장하고 Canary에서 이 값을 사용하게 된다.

canary는 buffer와 가까운 위치(첫번째 바이트)에 NULL 값이 포함되어 있다.
일부 문자열 입력 함수에서는 NULL 바이트를 입력할 수 없기 때문에 카나리 overwrite를 방지할 수 있지만, read 같은 함수는 NULL 바이트도 입력할 수 있어서 공격이 가능하다. 5dea0a70-1870-46f0-8469-f5c11b6478b7

검사는 함수 에필로그가 호출되며 canary 영역을 fs:0x28과 다시한번 검사해보고, 다르다면 변조된것으로 판단하고 __stack_chk_fail 을 호출한다.

0b7cbdf1-3a2e-40b9-a613-982230363013


TLS 세팅 확인 #

canary는 TLS영역에 있으며, 세그먼트 레지스터는 print나 info reg로 확인할 수 없기 때문에 arch_prctl을 호출할 때 체크해야 한다.

1pwndbg> catch syscall arch_prctl

init_tls() 함수에서 arch_prctl(ARCH_SET_FS, addr) 을 호출할 때를 확인해야 하는데, rdi가 0x1002 = ARCH_SET_FS 이고, rsi의 0x7ffff7fbe540이 레지스터에 세팅할 fs 주소라는 것을 확인할 수 있다.

a372f760-48dc-41d8-b1c4-72fe65ac7833

watch 명령으로 canary 영역인 0x7ffff7fbe540 + 0x28 위치에 값을 쓸때 멈추도록 하고, 출력해보면 어떤 값이 쓰여진 것을 확인할 수 있다. 나중에 canary 값이 세팅될 때 보면 이 값과 동일한 것을 확인할 수 있다.

1pwndbg> watch *(0x7ffff7fbe540 + 0x28)
2
3pwndbg> x/1gx 0x7ffff7fbe540 + 0x28
40x7ffff7fbe568: 0x3eb164530bcd3e00

우회방법 #

  1. 무차별 대입
    canary에는 1byte의 널값이 들어있어서 사실상 x86에서는 3byte, x64에서는 7byte만 대입해보면 되지만, 맞추기는 거의 불가능하다.

  2. TLS 읽기 또는 쓰기 TLS 값은 실행시 변경되지만, 값을 읽거나 쓸 수 있다면 임의의 값으로 조작하거나 읽어올 수 있다.

  3. 스택 카나리 릭 가장 현실적인 방법이며, 오버플로우로 인해 canary 값이 노출된다면, 이 값을 이용할 수 있다. 프로세스가 재실행 될 때마다 새로운 값으로 세팅되기 때문에 한번의 실행에서 누출 + 공격이 가능해야한다. (같은 스레드에서 두번의 BOF가 존재해야함)

    • canary 릭을 유도할 때 값을 출력해야 하기 때문에 canary의 \x00 까지 overwrite 해야 한다.
    • 문자열 출력은 낮은주소 → 높은주소로 읽기 때문에 첫번째 바이트를 건너뛰고 7바이트를 읽어와야 한다.

NX (No-eXecute) #

개요 #

쉘코드 공격이 수행되려면 메모리에 wx 권한을주고 코드 작성 후 실행까지 가능해야 하기 때문에 NX 비트를 설정해서 특정 메모리 영역을 페이지단위로 실행권한을 제거할 수 있다.
NX는 CPU가 지원한다면 기본적으로 세팅되며, 컴파일 시 -zexecstack 옵션으로 스택의 NX비트를 제거할 수 있다.
checksec을 통해서 확인할 수 있고 인텔은 XD(eXecute Disable), AMD는 NX, 윈도우는 DEP(Data Execution Prevention) ARM에서는 XN(eXecute Never) 라고 부른다.

  • 커널 5.4.0 미만 버전에서는 NX를 사용하지 않으면 READ_IMPLIES_EXEC 플래그를 세팅해서 힙, 데이터, 스택 등 r 권한이 있는 모든 메모리 영역에 x 권한이 추가된다. NX를 사용할때 제거된다.
  • 커널 5.4.0 이상 버전은 READ_IMPLIES_EXEC 플래그 없이 로더가 스택 영역에서만 x 권한을 추가한다.
 1$ checksec ./nx
 2[*] '/home/dreamhack/nx'
 3    Arch:     amd64-64-little
 4    RELRO:    Partial RELRO
 5    Stack:    Canary found
 6    NX:       NX enabled
 7    PIE:      No PIE (0x400000)
 8
 9# NX가 설정된 파일
10pwndbg> vmmap
11    0x7ffffffde000     0x7ffffffff000 rw-p    21000      0 [stack]
12
13# NX가 설정되지 않은 파일. stack 영역에 rwx 권한 모두 포함됨
14
15    0x7ffffffde000     0x7ffffffff000 rwxp    21000      0 [stack]

우회방법 #

NX는 실행과 쓰기 권한을 한꺼번에 주지 않는 방식이며, 메모리에 쉘 코드를 삽입하고 실행하는 방식을 보호하기 위한 기법이다.

  1. return to libc
    libc는 기본적인 함수들 (open, read, write, system 등)이 포함된 공유 라이브러리이며, 프로그램이 libc 함수를 사용하면서 지정된 preload 라이브러리가 없다면 동적링크 라이브러리(ld) 이후 가장먼저 로드된다.(대부분 libc를 의존하기 때문)
    스택의 return 주소를 덮는건 NX가 적용돼도 가능하기 때문에 쉘코드를 올리는 대신 libc를 사용해서 이미 로드된 함수를 사용하면 w 권한 없이도 코드 영역의 x 권한으로만 공격할 수 있다.

  2. ROP, JOP, Blind ROP
    RTL을 발전시켰으며, 원하는 함수가 없는 경우 여러개의 작은 코드(리턴가젯)을 프로그램 x 권한 메모리 영역 내에서 찾아 원하는 함수를 만들어내는 기법이다.
    Blind 방식은 메모리 레이아웃이 변하지 않는 시스템에서 일부러 크래시를 내서 ROP 가젯을 찾아내는 방법이다.
    리턴 가젯은 작은 명령어 이후 ret 하는 형식의 코드이며, ROPgadget 명령어로 프로그램 내부의 가젯들을 스캔할 수 있다. 가젯들을 모아서 명령실행 → ret 을 반복하다 보면 원하는 코드를 실행시킬 수 있게된다.

  3. return to PLT
    라이브러리(공유라이브러리 영역)를 직접 호출하는 return to libc와 달리 PLT영역(코드영역)을 사용해서 호출하는 방식이다.


ASLR (Address Space Layout Randomization) #

개요 #

바이너리가 실행될 때마다 힙, 스택, 공유라이브러리를 임의의 가상 메모리 주소에 할당하는 보호기법이다.
메모리는 페이지 단위(12bit)로 할당이 이뤄져서 ASLR이 세팅되고 계속 재실행 하더라도 영역의 베이스는 0x...000 이 되고, 라이브러리 내의 오프셋은 변하지 않아서 함수들의 주소도 하위 12bit는 동일한 특성이 있다.
커널에서 지원하는 보호 기법으로, 설정파일을 확인해서 적용 여부를 판단할 수 있다.

1$ cat /proc/sys/kernel/randomize_va_space
22
  • 0 : No ASLR
  • 1 : 스택, 힙, 라이브러리(라이브러리의 시작주소만 램덤화되고, 심볼들의 오프셋은 동일), vdso(시스템 콜 없이 커널기능에 일부 접근할 수 있도록 하는 공유 객체)에 ASLR 적용.
    힙, 스택, vdso, 공유라이브러리 ‘영역’의 시작 주소를 ASLR화 한다.
  • 2 : 1의 옵션에 brk 영역도 포함.
    전통적인 힙 할당은 bss 세그먼트 아래의 brk라는 포인터를 둬서 필요한만큼 이동시켜 연속적인 힙 영역을 할당한다.
    현대에는 작은 메모리를 할당할때만 brk를 사용하고, 큰 메모리는 mmap을 통해 비연속적인 힙 영역을 할당하도록 구현되어 있다.
    bss에 랜덤한 오프셋을 추가해서 brk를 사용하는 방법으로 brk에 ASLR을 적용한다

9a168578-e9df-4281-bba9-e12a02160025


PIE (Position Independent Executable) #

프로그램이 어떤 위치에 로드되더라도 정상적으로 동작할 수 있도록 상대주소로 프로그램이 컴파일되는 방식이다.

과거에는 상대주소는 주소를 계산해야되는 오버헤드가 생기기 때문에 프로그램이 가상메모리에 고정적인 위치에 로드되도록 설계되어 있어서 절대주소를 사용하도록 컴파일됐었다.

PIE가 적용되지 않은 ASLR을 코드와 데이터 섹션에는 적용할 수 없어서 처음부터 상대주소를 사용했던 스택, 힙, 공유라이브러리 정도만 ASLR이 우선 적용된 것이고, PIE가 적용된 프로그램은 코드영역도 상대주소기 때문에 코드와 데이터 영역도 ASLR이 적용된다.

현대의 gcc는 기본적으로 컴파일 시 PIE가 적용되어 있다.

return to PLT 공격의 장점 #

PLT 영역은 코드영역에 있어서 ASLR이 적용되더라도 PIE 방식이 아니라면 PLT 주소가 절대주소로 참조되기 때문에 return to libc보다 return to PLT가 더 쉽게 공격할 수 있게 된다.


우회방법 #

실행할 때마다 주소가 변경되기 때문에 한번의 실행에 공격을 성공해야 한다.
PIE가 적용되지 않은 ASLR은 코드영역과 데이터영역은 고정주소이기 때문에 그 주소는 한번에 가져오지 않아도 된다.

PIE가 적용되지 않은 ASLR 우회 #

  1. return to PLT
    PLT 영역은 코드 섹션이기 때문에 PIE가 적용되지 않은 파일의 경우 PLT로 점프하면 원하는 라이브러리 함수를 호출할 수 있다.
    PLT 영역에는 프로그램 내에서 호출해야만 등록되기 때문에 보통 system 함수는 여러 IDE에서 경고를 발생시켜 사용을 지양하도록 한다.
    라이브러리는 메모리에 전체가 매핑되므로 이 경우엔 system 함수의 주소를 직접 구한다면 호출할 수 있다.
    같은 libc 라이브러리라면 함수의 offset도 같기 때문에 함수간 거리도 같다는 특성을 이용하면 호출하지 않은 함수도 구할 수 있다.
    “/bin/sh” 문자열도 역시 libc 라이브러리에서 가져올 수 있다.

  2. GOT overwrite
    gadget으로 인자들을 넣은 뒤 write 함수를 호출해서 GOT 영역을 찾고, read 함수를 호출해서 GOT 영역을 overwrite한다.
    세개의 인자를 받기 때문에 rdi, rsi, rdx를 전달해줘야 하는데, rdi, rsi를 설정하는 gadget은 많지만, rdx는 설정하기 어렵다.
    main 호출 전에 libc의 초기화 코드가 실행된다. GLIBC 2.34 미만에서는 __libc_csu_init()을 호출하게 되는데 gadget이 많이 발견되기 때문에 __libc_csu_init() 함수가 삭제됐다.
    GLIBC 2.34 부터는 다른 함수(strncmp 등)를 호출해서 rdx를 변화시켜 원하는 값을 설정할 수 있고, 이미 호출된 프로그램 상태에서 rdx가 적당한 값으로 변경되어 있을 수 있다.
    got는 실제 함수의 주소를 저장하고있는 메모리 위치이고, plt는 got를 이용해서 동적링커나 실제 함수 주소로 점프하는 코드가 담긴 위치이기 때문에 return address로는 plt만 사용할 수 있다.

  • GLIBC 2.31 라이브러리 환경에서 컴파일한 경우 3abb6a30-bf2e-4a06-a197-edf340906515

  • GLIBC 2.35 라이브러리 환경에서 컴파일한 경우 바이너리에 __libc_csu_init이 제거되어 있음 601390fc-e8df-4352-b77e-8f0e3c24b626

  1. ret2main
    취약한 부분이 한곳만 있는 경우엔 ASLR 같은 기법이 적용된 경우엔 공격하기 어려워진다. 사실상 ROP 가젯만 있다면 스택이 소진될 때 까지 공격이 가능하지만, rdx(count)도 세팅할 수 없는 상황이고, 버퍼가 한정적인 경우엔 다시 취약한 메인으로 돌아오면 무제한으로 공격이 가능하다.

PIE가 적용된 ASLR 우회 #

  1. 코드베이스 구하기
    라이브러리 대상으로 공격했던 것 처럼 코드영역의 베이스 주소를 읽고 그 주소에서 오프셋을 구해와야 한다.

  2. Partial Overwrite
    코드 베이스를 구하기 어렵다면 일부 바이트만 덮는 방법도 사용할 수 있다.
    ASLR 특성 상 하위 12바이트 값은 같기 때문에 가젯이 하위 1byte만 다르다면, 기존 주소에 1byte만 덮어씌워서 원하는 코드를 실행시킬 수 있을것이다.
    2byte가 다르다면, 4bit 정도는 brute force로 맞출 수 있게된다.


RELRO (RELocation ReadOnly) #

쓰기권한이 있을때 공격당하기 좋은(실행 흐름이 변경될 수 있는) 메모리 영역을 ReadOnly로 설정하는 보호기법이다.
init이나 fini, got 등의 영역은 쓰기권한이 있을때 취약하다.

Partial RELRO #

취약하더라도 프로그램 실행 시 필요한 부분만 남겨두고 ReadOnly로 설정한다. 23c5adfe-1fe0-44d5-b86b-d1bf2b063d96

maps 로 출력된 메모리 영역을 확인해보면 403000-404000 은 쓰기권한이 없고, 404000-405000 은 쓰기권한이 있는것을 알 수 있다.

89a041ee-15be-4a04-ae9b-c0bbb7ce7e91

init_array, fini_array, dynamic, got는 403000-404000 영역에 해당하고 got.plt, data, bss는 404000-405000 영역에 해당하는 것을 알 수 있다.
Partial RELRO가 적용되면 got와 got.plt로 나뉘는데, got는 주로 전역변수나 정적 데이터의 주소 등을 저장해 바이너리 로딩 시점에 바인딩되는 값들이 포함되어 더이상 런타임에 수정할 필요가 없는 영역이고,
got.plt는 주로 함수 주소를 저장하며 런타임 중 resolver에 의해 처음 호출될 때 쓰기가 가능해야하기 때문에 나눠지게 됐다.


Full RELRO #

코드의 흐름을 변경할 수 있는 전체 영역을 ReadOnly로 변경한다.
got도 got.plt처럼 나눠지지 않고 모든 영역이 쓰기 권한이 없는 메모리로 매핑되어있는 것을 볼 수 있는데, 라이브러리 함수의 주소가 바이너리 로딩 시점에 즉시 바인딩 된다는 의미이다.

49e4d042-e2d7-4225-855d-c687f0a58cae

프로그램 이미지 베이스는 560560bbe000이며, .got의 offset 2fa8 을 더하면 560560bc0fa8 이다. 이 값은 ReadOnly 영역이다.

dd6f1dab-622b-4ea4-a0bf-c51ecc1f4566


우회방법 #

.got, .init_array, .fini_array 가 쓰기 권한이 완전히 제거됐기 때문에 이런 영역들의 overwrite를 더이상 사용할 수 없게 됐다.

  1. hook overwrite
    결국엔 함수 포인터가 저장된 위치가 쓰기 가능한 영역이라면 언제든 overwrite 해서 호출될 때를 노려 공격하는 방식이 가능하다.
    malloc 함수의 코드를 살펴보면 __malloc_hook이 존재한다면 hook 함수를 호출하게 구현되어 있는데, 이 영역은 libc.so 라이브러리의 쓰기 가능 영역(.bss)에 해당한다.
    이런 hook들을 overwrite 하고, malloc을 호출하면 자동으로 공격 흐름을 변경할 수 있게 된다.
    libc의 쓰기 영역에 존재하는 함수 포인터라서 취약하기도 하고, 성능상 효율이 떨어져 GLIBC 2.34 버전부터 hook이 삭제됐다.
    원래 의도는 malloc, free, pthread_create 등을 호출할 때 모니터링하는 등의 작업을 하기 위해서 구현되어 있었다.
  • libc 2.31 버전 2cba7a9f-c0ca-4b39-9cb0-d25fc71c07ef

  • libc 2.35 버전 5a68e0b9-19bf-47ab-a7cb-d2a8803362de

 1// malloc의 구현부
 2void *__libc_malloc (size_t bytes)
 3{
 4  mstate ar_ptr;
 5  void *victim;
 6  void *(*hook) (size_t, const void *)
 7    = atomic_forced_read (__malloc_hook); // malloc hook read
 8  if (__builtin_expect (hook != NULL, 0))
 9    return (*hook)(bytes, RETURN_ADDRESS (0));  // 훅 호출하면서 malloc의 인자 전달
10  ...
comments powered by Disqus