본문 바로가기

3. Pwnable (포너블)/2) 개념 정리

[24.05.11] 6주차 활동- 포너블 개념

1. 드림핵 'System Hacking' 로드맵을 함께 듣고 공부했다.

  • Stack Buffer Overflow
  • Stack Canary
  • Bypass NX & ASLR

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

 

System Hacking

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

dreamhack.io


[ Stack Buffer Overflow ]

1. Background : 함수 호출 규약

(1) 함수 호출 규약

: 함수 호출 규약은 함수의 호출 및 반환에 대한 약속, 한 함수에서 다른 함수를 호출할 때 프로그램의 실행 흐름은 다른 함수로 이동하고 호출한 함수가 반환하면, 다시 원래의 함수로 돌아와서 기존의 실행 흐름을 이어나감. 그러므로 함수를 호출할 때는 반환된 이후를 위해 호출자의 상태 및 반환 주소를 저장해야함. 또한, 호출자는 피호출자가 요구하는 인자를 전달해줘야 하며, 피호출자의 실행이 종료될 때는 반환 값을 전달받아야 함, 함수 호출 규약을 적용하는 것은 일반적으로 컴파일러가 함.

 

(2) 함수 호출 규약의 종류

: CPU의 아키텍처가 같아도 컴파일러가 다르면 적용하는 호출 규약이 다를 수 있음. (ex. C언어를 컴파일할 때, 윈도우에서는 MSVC를, 리눅스에서는 gcc를 많이 사용 →  이 둘은 같은 아키텍처에 대해서도 다른 호출 규약을 적용함. x86-64 아키텍처에서 MSVC는 MS x64 호출 규약, gcc는 SYSTEM V 호출 규약을 적용함.) 이 때, 컴파일러는 지원하는 호출 규약 중, CPU 아키텍처에 적합한 것을 선택함.

 

(3) x86-64호출 규약: SYSV

: 리눅스는 SYSTEM V(SYSV) Application Binary Interface(ABI)를 기반으로 만들어짐. SYSV ABI는 ELF 포맷, 링킹 방법, 함수 호출 규약 등의 내용을 담고 있음.

(4) x86-64호출 규약: cdecl

: x86아키텍처는 레지스터의 수가 적으므로 스택을 통해 인자를 전달함, 인자를 전달하기 위해 사용한 스택을 호출자가 정리하는 특징이 있는데 스택을 통해 인자를 전달할 때는, 마지막 인자부터 첫 번째 인자까지 거꾸로 스택에 push함. (ex. function(1,2,3) 이라면 스택에는 3 → 2 → 1 순서로 push 함.)

 

2. Stack Buffer Overflow : 스택 버퍼 오버플로우

(1) 스택 버퍼 오버플로우 

: 문자 그대로 버퍼가 넘치는 것, 버퍼는 제각기 크기를 가지고 있는데 int로 선언한 지역 변수는 4바이트의 크기를 갖고, 10개의 원소를 갖는 char배열은 10바이트의 크기를 갖는다. 만약 10바이트 크기의 버퍼에 20바이트 크기의 데이터가 들어가려 하면 오버플로우가 발생하는 것!

★ 버퍼? 데이터가 목적지로 이동되기 전에 보관되는 임시 저장, 데이터의 처리속도가 다른 두 장치가 있을 때 이 둘 사이에 오가는 데이터를 임시로 저장해 두는일종의 완충 작용을 함 → 버퍼가 가득 찰 때까지는 유실되는 데이터 없이 통신할 수 있음

스택 오버플로우 스택 버퍼 오버플로우
스택 영역이 너무 많이 확장돼서 발생하는 버그 스택에 위치한 버퍼에 버퍼의 크기보다 많은 데이터가 입력되어 발생하는 버그

 

 

스택 버퍼 오버플로우 예제 코드

// Name: sbof_auth.c
// Compile: gcc -o sbof_auth sbof_auth.c -fno-stack-protector
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int check_auth(char *password) {
    int auth = 0;
    char temp[16];
    
    strncpy(temp, password, strlen(password));
    
    if(!strcmp(temp, "SECRET_PASSWORD"))
        auth = 1;
    
    return auth;
}
int main(int argc, char *argv[]) {
    if (argc != 2) {
        printf("Usage: ./sbof_auth ADMIN_PASSWORD\n");
        exit(-1);
    }
    
    if (check_auth(argv[1]))
        printf("Hello Admin!\n");
    else
        printf("Access Denied!\n");
}

 

(2) 스택 버퍼 오버플로우 예시

: 데이터 변조 - 버퍼 오버플로우가 발생하는 버퍼 뒤에 중요한 데이터가 있다면, 해당 데이터가 변조됨으로써 문제가 발생할 수 있다. 예를 들어, 입력 데이터에서 악성 데이터를 감지하여 경고해주는 프로그램이 있을 때, 악성의 조건이 변경되면 악성 데이터에도 알람이 울리지 않을 수 있음.

 

실행흐름 조작 - 함수 호출의 내용을 되짚어 보면, 함수를 호출할 때 반환 주소를 스택에 쌓고, 함수에서 반환될 때 이를 꺼내어 원래의 실행 흐름으로 돌아감. 이를 공격자의 관점에서 바라보면, '스택 버퍼 오버플로우로 반환 주소를 조작하면 어떻게 될까'라고 생각할 수 있음. 실제로, 함수의 반환 주소를 조작하면 프로세스의 실행 흐름을 바꿀 수 있음.

 

(3) 스택 버퍼 오버플로우 실습


Stack Canary ]

1. Stack Canary : 스택 카나리

(1) 스택 카나리

: 스택 버퍼 오버플로우로부터 반환 주소를 보호하는 것임, 함수의 프롤로그에서 스택 버퍼와 반환 주소 사이에 임의의 값을 삽입하고 함수의 에필로그에서 해당 값의 변조를 확인하는 보호 기법임. 스택 버퍼 오버플로우로 반환 주소를 덮으려면 반드시 카나리를 먼저 덮어야하므로 카나리 값을 모르는 공격자는 카나리 값을 변조하게 되는데 이 때, 에필로그에서 변조가 확인되어 공격자는 실행 흐름을 획득하지 못함.

 

(2) 카나리의 작동원리

: 강의에 코드 보며 이해하기

 

(3) 카나리 생성 과정

: security_init 함수에서 TLS에 랜덤 값으로 카나리를 설정하면 매 함수에서 이를 참조해서 사용하게 됨.

 

(4) 카나리 우회

: 무차별 대입 - x64 아키텍처는 8비트, x86 아키텍처에는 4바이트의 카나리가 생성되고 각각의 카나리에는 NULL이 포함되어있다. 따라서 실제로 7바이트와 3바이트의 랜덤한 값이 포함됨. 무차별 대입으로 카나리 값을 알아내려면 각각 256^7, 256^3번의 연산이 필요함. 그러므로 실제 서버를 대상으로 저 횟수로 무차별 대입을 시도하는 것은 불가능함.

 

  TLS 접근 - 카나리는 TLS에 전역변수로 저장되고 매 함수마다 이를 참조해서 사용함. TLS 주소는 매 실행마다 바뀌지만 실행 중에 이 주소를 알 수 있다면 TLS에 설정된 카나리 값을 읽거나 이를 임의의 값으로 조작할 수 있음. 그 후에 스택 버퍼 오버플로우를 수행할 때 알아낸 카나리 값이나 조작한 값으로 스택 카나리를 덮으면 카나리 검사를 우회할 수 있음.

 

 스택 카나리 릭 - 함수의 프롤로그에 스택에 카나리 값을 저장하므로 이를 읽어낸다면 카나리 우회 가능 → 가장 현실적인 기법

 

스택 카나리 우회 예제 코드 (스택 카나리 릭)

// Name: bypass_canary.c
// Compile: gcc -o bypass_canary bypass_canary.c
#include <stdio.h>
#include <unistd.h>
int main() {
  char memo[8];
  char name[8];
  printf("name : ");
  read(0, name, 64);
  printf("hello %s\n", name);
  printf("memo : ");
  read(0, memo, 64);
  printf("memo %s\n", memo);
  return 0;
}

 


[ Bypass NX & ASLR ]

1. Background

어떤 보호 기법이 등장하면 이를 우회하는 새로운 공격 기법이 등장함. 어떤 공격이 새롭게 등장할지는 아무도 모르므로 시스템 개발자들은 여러 겹의 보호 기법을 적용하여 시스템이 공격당할 수 있는 표면자체를 줄여나가려고 했음. 이와 관련하여 개발자들은 Address Space Layout Randomization(ASLR)과 No-eXecute(NX)을 개발하고, 시스템에 적용함.

2. No-eXecute : NX

NX는 실행에 사용되는 메모리 영역과 쓰기에 사용되는 메모리 영역을 분리하는 보호 기법임. 어떤 메모리 영역에 대해 쓰기 권한과 실행 권한이 함께 있으면 시스템이 취약해지기 쉬움. (ex.코드 영역에 쓰기 권한이 있으면 공격자는 코드를 수정하여 원하는 코드가 실행되게 할 수 있음.) CPU가 NX를 지원하면 컴파일러 옵션을 통해 바이너리에 NX를 적용할 수 있으며, NX가 적용된 바이너리는 실행될 때 각 메모리 영역에 필요한 권한만을 부여받음. gdb의 vmmap으로 NX 적용 전후의 메모리 맵을 비교하면, 다음과 같이 NX가 적용된 바이너리에는 코드 영역 외에 실행 권한이 없는 것을 확인할 수 있음. 반면, NX가 적용되지 않은 바이너리에는 스택 영역에 실행 권한이 존재하여 rwx 권한을 가지고 있음을 확인할 수 있음.

 

pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
             Start                End Perm     Size Offset File
          0x400000           0x401000 r--p     1000      0 /home/dreamhack/nx
          0x401000           0x402000 r-xp     1000   1000 /home/dreamhack/nx
          0x402000           0x403000 r--p     1000   2000 /home/dreamhack/nx
          0x403000           0x404000 r--p     1000   2000 /home/dreamhack/nx
          0x404000           0x405000 rw-p     1000   3000 /home/dreamhack/nx
    0x7ffff7d7f000     0x7ffff7d82000 rw-p     3000      0 [anon_7ffff7d7f]
    0x7ffff7d82000     0x7ffff7daa000 r--p    28000      0 /usr/lib/x86_64-linux-gnu/libc.so.6
    0x7ffff7daa000     0x7ffff7f3f000 r-xp   195000  28000 /usr/lib/x86_64-linux-gnu/libc.so.6
    0x7ffff7f3f000     0x7ffff7f97000 r--p    58000 1bd000 /usr/lib/x86_64-linux-gnu/libc.so.6
    0x7ffff7f97000     0x7ffff7f9b000 r--p     4000 214000 /usr/lib/x86_64-linux-gnu/libc.so.6
    0x7ffff7f9b000     0x7ffff7f9d000 rw-p     2000 218000 /usr/lib/x86_64-linux-gnu/libc.so.6
    0x7ffff7f9d000     0x7ffff7faa000 rw-p     d000      0 [anon_7ffff7f9d]
    0x7ffff7fbb000     0x7ffff7fbd000 rw-p     2000      0 [anon_7ffff7fbb]
    0x7ffff7fbd000     0x7ffff7fc1000 r--p     4000      0 [vvar]
    0x7ffff7fc1000     0x7ffff7fc3000 r-xp     2000      0 [vdso]
    0x7ffff7fc3000     0x7ffff7fc5000 r--p     2000      0 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
    0x7ffff7fc5000     0x7ffff7fef000 r-xp    2a000   2000 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
    0x7ffff7fef000     0x7ffff7ffa000 r--p     b000  2c000 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
    0x7ffff7ffb000     0x7ffff7ffd000 r--p     2000  37000 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
    0x7ffff7ffd000     0x7ffff7fff000 rw-p     2000  39000 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
    0x7ffffffde000     0x7ffffffff000 rw-p    21000      0 [stack]
0xffffffffff600000 0xffffffffff601000 --xp     1000      0 [vsyscall]

NX Enabled

pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
             Start                End Perm     Size Offset File
          0x400000           0x401000 r--p     1000      0 /home/dreamhack/nx_disabled
          0x401000           0x402000 r-xp     1000   1000 /home/dreamhack/nx_disabled
          0x402000           0x403000 r--p     1000   2000 /home/dreamhack/nx_disabled
          0x403000           0x404000 r--p     1000   2000 /home/dreamhack/nx_disabled
          0x404000           0x405000 rw-p     1000   3000 /home/dreamhack/nx_disabled
    0x7ffff7d7f000     0x7ffff7d82000 rw-p     3000      0 [anon_7ffff7d7f]
    0x7ffff7d82000     0x7ffff7daa000 r--p    28000      0 /usr/lib/x86_64-linux-gnu/libc.so.6
    0x7ffff7daa000     0x7ffff7f3f000 r-xp   195000  28000 /usr/lib/x86_64-linux-gnu/libc.so.6
    0x7ffff7f3f000     0x7ffff7f97000 r--p    58000 1bd000 /usr/lib/x86_64-linux-gnu/libc.so.6
    0x7ffff7f97000     0x7ffff7f9b000 r--p     4000 214000 /usr/lib/x86_64-linux-gnu/libc.so.6
    0x7ffff7f9b000     0x7ffff7f9d000 rw-p     2000 218000 /usr/lib/x86_64-linux-gnu/libc.so.6
    0x7ffff7f9d000     0x7ffff7faa000 rw-p     d000      0 [anon_7ffff7f9d]
    0x7ffff7fbb000     0x7ffff7fbd000 rw-p     2000      0 [anon_7ffff7fbb]
    0x7ffff7fbd000     0x7ffff7fc1000 r--p     4000      0 [vvar]
    0x7ffff7fc1000     0x7ffff7fc3000 r-xp     2000      0 [vdso]
    0x7ffff7fc3000     0x7ffff7fc5000 r--p     2000      0 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
    0x7ffff7fc5000     0x7ffff7fef000 r-xp    2a000   2000 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
    0x7ffff7fef000     0x7ffff7ffa000 r--p     b000  2c000 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
    0x7ffff7ffb000     0x7ffff7ffd000 r--p     2000  37000 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
    0x7ffff7ffd000     0x7ffff7fff000 rw-p     2000  39000 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
    0x7ffffffde000     0x7ffffffff000 rwxp    21000      0 [stack]
0xffffffffff600000 0xffffffffff601000 --xp     1000      0 [vsyscall]

NX Disabled

 

Checksec 명령 ./nx(nx_disabled) 을 이용해 바이너리에 NX가 적용됐는지 확인할 수 있음. 

+)NX을 인텔은 XD, 윈도우는 DEP 등이라고 칭하는데 모두 비슷한 보호 기법임

 

3. Address Space Layout Randomization : ASLR

(1) 바이너리가 실행될 때마다 스택, 힙 등을 임의의 주소에 할당하는 보호 기법임. 커널에서 지원하는 보호 기법이며 다음의 명령어로 확인할 수 있음.

$ cat /proc/sys/kernel/randomize_va_space
2 (0,1 값을 가질 수 있음)

 

 각 ASLR이 적용되는 메모리 영역

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

(2) 예제코드로 ASLR 특징 살펴보기

 

(3) 정리

: NX와 ASLR이 적용되면 스택, 힙, 데이터 영역에는 실행 권한이 제거되며 이들이 할당되는 주소가 계속 변함. 그러나 바이너리의 코드가 존재하는 영역은 여전히 실행 권한이 존재하며, 할당되는 주소도 고정되어 있음.

NX ASLR
프로세스의 각 세그먼트에 필요한 권한만 부여하는 보호 기법. 일반적으로 코드 영역에는 읽기와 실행을, 나머지 영역에는 읽기와 쓰기 권한이 부여됨. 메모리를 무작위 주소에 할당하는 보호 기법. 최신 커널들은 대부분 적용되어 있고, 리눅스에서는 페이지 단위로 할당이 이루어지므로 하위 12비트는 변하지 않는다는 특징이 있음.

 

4. Library

라이브러리는 컴퓨터 시스템에서, 프로그램들이 함수나, 변수를 공유해서 사용할 수 있게 함. 대개의 프로그램은 서로 공통으로 사용하는 함수들이 많습니다. 예를 들어, printf, scanf, malloc 등은 많은 C 프로그래머들이 코드를 작성하면서 사용하는 함수임. 많은 컴파일 언어들은 자주 사용되는 함수들의 정의를 묶어서 하나의 라이브러리 파일로 만들고, 이를 여러 프로그램이 공유해서 사용할 수 있도록 지원하고 있음. 라이브러리를 사용하면 같은 함수를 반복적으로 정의하지 않아도 되므로 코드 개발의 효율이 높아진다는 장점이 있음. 라이브러리는 크게 동적, 정적으로 구분됨.

5. Link

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

 

- 동적 링크:  동적 링크된 바이너리를 실행하면 동적 라이브러리가 프로세스의 메모리에 매핑되됨. 실행 중에 라이브러리의 함수를 호출하면 매핑된 라이브러리에서 호출할 함수의 주소를 찾고, 그 함수를 실행함.

 

-정적 링크: 정적 링크를 하면 바이너리에 정적 라이브러리의 필요한 모든 함수가 포함됨. 따라서 해당 함수를 호출할 때, 라이브러리를 참조하는 것이 아니라, 자신의 함수를 호출하는 것처럼 호출할 수 있음. 여러 바이너리에서 라이브러리를 사용하면  복제가 여러 번 이루어지게 되므로 용량을 낭비함.