본문 바로가기

3. Pwnable (포너블)

[2024.11.02]PWN PWN 해 4주차 활동

4주차는 드림핵의 System Hacking 로드맵에서 Stack Canary, Bypass NX & ASLR 강의를 공부했다.

https://dreamhack.io/lecture/roadmaps/2

 

System Hacking

시스템 해킹을 공부하기 위한 로드맵입니다.

dreamhack.io

 

Mitigation: Stack Canary

 

스택 카나리(Stack Canary): 스택 버퍼 오버플로우로부터 반환 주소를 보호하는 기법

스택 버퍼 오버플로우로 반환 주소를 덮으려면 반드시 카나리를 먼저 덮어야 하므로 카나리 값을 모르는 공격자는 반환 주소를 덮을 때 카나리 값을 변조하게 됨. 이 경우, 에필로그에서 변조가 확인되어 공격자는 실행 흐름을 획득하지 못함.

 

카나리의 작동 원리

 

카나리 정적 분석

카나리 비활성화

$ gcc -o no_canary canary.c -fno-stack-protector //옵션 추가

$ ./no_canary

HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH //길이가 긴 입력

Segmentation fault // 잘못된 메모리 참조 에러

카나리 활성화

$ gcc -o canary canary.c // 카나리 적용

$ ./canary

HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH

*** stack smashing detected ***: <unknown> terminated

Aborted // 스택 버퍼 오버플로우가 탐지되어 프로세스 강제종료

 

카나리 동적 분석

카나리 저장

pwndbg> ni

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

   0x5555555546bb <main+17> mov qword ptr [rbp - 8], rax

   0x5555555546bf <main+21> xor eax, eax

pwndbg> print /a $rax

$1 = 0xf80f605895da3c00 //rax에 첫 바이트가 널 바이트인 8바이트 데이터가 저장

pwndbg> x/gx $rbp-0x8

0x7fffffffe238: 0xf80f605895da3c00 //생성한 랜덤값은 main+17에서 rbp-0x8에 저장

 

카나리 검사

pwndbg> break *main+50

pwndbg> continue

HHHHHHHHHHHHHHHH

Breakpoint 2, 0x00000000004005c8 in main ()

   0x5555555546dc <main+50> mov rcx, qword ptr [rbp - 8] <0x7ffff7af4191> //rbp-8에 저장한 카나리를 rcx로 옮김

   0x5555555546e0 <main+54> xor rcx, qword ptr fs:[0x28] //rcx fs:0x28에 저장된 카나리와 xor

   0x5555555546e9 <main+63> je main+70 <main+70> //두 값이 동일하면 연산 결과가 0이 되면서 je

   

   0x5555555546f0 <main+70> leave

   0x5555555546f1 <main+71> ret //main 함수에서 정상적으로 반환

 

카나리 생성 과정

 

TLS의 주소 파악

fs TLS를 가리키므로 fs의 값을 알면 TLS의 주소를 알 수 있음.

gdb에는 특정 이벤트가 발생했을 때, 프로세스를 중지시키는 catch.

$ gdb -q ./canary

pwndbg> catch syscall arch_prctl

Catchpoint 1 (syscall 'arch_prctl' [158]) //arch_prctl catchpoint를 설정

pwndbg> run //실행

ð  init_tls() 안에서 catchpoint에 도달할 때까지 continue 명령어를 실행

 

카나리 값 설정

watch는 특정 주소에 저장된 값이 변경되면 프로세스를 중단시키는 명령어

pwndbg> watch *(0x7ffff7d7f740+0x28)

Hardware watchpoint 4: *(0x7ffff7d7f740+0x28)

watchpoint를 설정하고 프로세스를 계속 진행시키면 security_init 함수에서 프로세스가 멈춤.

TLS+0x28의 값을 조회하면 0x8ab7f53277873d00이 카나리로 설정된 것을 확인.

pwndbg> b *main

Breakpoint 3 at 0x555555555169

실제로 이 값이 main 함수에서 사용하는 카나리값인지 확인하기 위해 main 함수에 중단점을 설정하고, 계속 실행.

 

카나리 우회

 

무차별 대입 (Brute Force)

x64 아키텍처에서는 8바이트의 카나리가 생성되며, x86 아키텍처에서는 4바이트의 카나리가 생성.

, 무차별 대입으로 x64 아키텍처의 카나리 값을 알아내려면 최대 2567 , x86 에서는 최대 2563 번의 연산이 필요.

TLS 접근

카나리는 TLS에 저장되며 카나리에 의해서 보호되는 함수마다 이를 참조해 사용.

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

스택 카나리 릭

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

 

Mitigation: NX & ASLR

 

No-eXecute(NX): 실행에 사용되는 메모리 영역과 쓰기에 사용되는 메모리 영역을 분리하는 보호 기법.

CPU NX를 지원하면 컴파일러 옵션을 통해 바이너리에 NX를 적용.

gdb vmmap으로 NX 적용 전후의 메모리 맵을 비교하면, 다음과 같이 NX가 적용된 바이너리에는 코드 영역 외에 실행 권한이 없는 것을 확인. 반면, NX가 적용되지 않은 바이너리에는 스택 영역([stack])에 실행 권한이 존재하여 rwx 권한을 가지고 있음.

 

Checksec을 이용한 NX 확인

checksec을 이용하면 바이너리에 NX가 적용됐는지 확인할 수 있음.

$ checksec ./nx

[*] '/home/dreamhack/nx'

    Arch:     amd64-64-little

    RELRO:    Partial RELRO

    Stack:    Canary found

    NX:       NX enabled

    PIE:      No PIE (0x400000)

(NX를 인텔은 XD(eXecute Disable) , AMD NX, 윈도우는 DEP(Data Execution Prevention) , ARM에서는 XN(eXecute Never) 라고 칭함.)

 

Return to Shellcode w/t NX

 

Address Space Layout Randomization(ASLR): 바이너리가 실행될 때마다 스택, , 공유 라이브러리 등을 임의의 주소에 할당하는 보호 기법.

$ cat /proc/sys/kernel/randomize_va_space

2 //이 값은 0, 1, 2의 값을 가질 수 있음

  • No ASLR(0): ASLR을 적용하지 않음
  • Conservative Randomization(1): 스택, 라이브러리, vdso
  • Conservative Randomization + brk(2): (1)의 영역과 brk로 할당한 영역

ASLR의 특징

1. 코드 영역의 main함수를 제외한 다른 영역의 주소들은 실행할 때마다 변경.
실행할 때 마다 주소가 변경되기 때문에 바이너리를 실행하기 전에 해당 영역들의 주소를 예측할 수 없음.

2. 바이너리를 반복해서 실행해도 libc_base 주소 하위 12비트 값과 printf 주소 하위 12비트 값은 변경되지 않음.
리눅스는 ASLR이 적용됐을 때, 파일을 페이지(page) 단위로 임의 주소에 매핑. 따라서 페이지의 크기인 12비트 이하로는 주소가 변경되지 않음.

3. libc_base printf의 주소 차이는 항상 같음.
ASLR
이 적용되면, 라이브러리는 임의 주소에 매핑. 그러나 라이브러리 파일을 그대로 매핑하는 것이므로 매핑된 주소로부터 라이브러리의 다른 심볼들 까지의 거리(Offset)는 항상 같음.

 

Background: Library - Static Link vs. Dynamic Lin

 

라이브러리

라이브러리를 사용하면 같은 함수를 반복적으로 정의해야 하는 수고를 덜 수 있음.

C의 표준 라이브러리인 libc는 우분투에 기본으로 탑재된 라이브러리.

 

링크(Link): 많은 프로그래밍 언어에서 컴파일의 마지막 단계. 프로그램에서 어떤 라이브러리의 함수를 사용한다면, 호출된 함수와 실제 라이브러리의 함수가 링크 과정에서 연결됨. // Name: hello-world.c

// Compile: gcc -o hello-world hello-world.c

#include <stdio.h>

int main() {

  puts("Hello, world!");

  return 0;

}

puts의 선언이 stdio.h에 있어서 심볼로는 기록되어 있지만, 심볼에 대한 자세한 내용은 하나도 기록되어 있지 않음. 심볼과 관련된 정보들을 찾아서 최종 실행 파일에 기록하는 것이 링크 과정에서 하는 일 중 하나.

 

라이브러리와 링크의 종류

동적 링크

실행 중에 라이브러리의 함수를 호출하면 매핑된 라이브러리에서 호출할 함수의 주소를 찾고, 그 함수를 실행.

정적 링크

바이너리에 정적 라이브러리의 필요한 모든 함수가 포함. 여러 바이너리에서 라이브러리를 사용하면 그 라이브러리의 복제가 여러 번 이루어지게 되므로 용량을 낭비.

 

동적 링크 vs. 정적 링크

용량

각각의 용량을 ls로 비교해보면 static dynamic 보다 50배 가까이 더 많은 용량을 차지.

호출 방법

static 에서는 puts 가 있는 0x40c140을 직접 호출.

dynamic 에서는 puts plt주소인 0x401040 을 호출.

 

PLT & GOT

PLT GOT

PLT(Procedure Linkage Table)GOT(Global Offset Table)는 라이브러리에서 동적 링크된 심볼의 주소를 찾을 때 사용하는 테이블.

 

시스템 해킹의 관점에서 본 PLT GOT

PLT에서 GOT를 참조하여 실행 흐름을 옮길 때, GOT의 값을 검증하지 않는다는 보안상의 약점.

puts GOT 엔트리에 저장된 값을 공격자가 임의로 변경할 수 있으면, puts가 호출될 때 공격자가 원하는 코드가 실행되게 할 수 있음.

이와 같이 GOT 엔트리에 임의의 값을 오버라이트(Overwrite)하여 실행 흐름을 변조하는 공격 기법을 GOT Overwrite라고 부름.

 

리턴 가젯

리턴 가젯(Return gadget): ret 명령어로 끝나는 어셈블리 코드 조각을 의미.

ROPgadget 명령어를 사용해서 가젯을 구할 수 있음.

$ ROPgadget --binary rtl
Gadgets information
============================================================
...
0x0000000000400285 : ret
...
 
Unique gadgets found: 83
$

 

리턴 가젯은 반환 주소를 덮는 공격의 유연성을 높여서 익스플로잇에 필요한 조건을 만족할 수 있도록 도움.

대부분의 함수는 ret로 종료되므로, 함수들도 리턴 가젯으로 사용될 수 있음.

ROP: 리턴 가젯을 사용하여 복잡한 실행 흐름을 구현하는 기법.