[Dreamhack] Type Error: 1 - Type Error

securitykss·2023년 2월 7일
0

Pwnable 강의(dreamhack)

목록 보기
35/58

https://dreamhack.io/lecture/courses/118 을 토대로 작성한 글입니다.

1. Introduction

자료형은 변수의 크기를 정의하고, 용도를 암시한다.

예를들어, int형으로 변수를 선언하면, 그 변수는 4바이트 크기를 갖고, 정수 연산에 사용될 변수임을 의미한다.

자료형이 담고 있는 정보는 컴파일러에도 전달된다.

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

int형 변수에는 4바이트 공간이 할당되고, char형 변수에는 1바이트 공간이 할당된다.

그리고 각 변수에 대한 연산은 그 메모리 공간을 대상으로 이뤄진다.

1.1 overflow

한 번 정의된 변수의 자료형은 바꿀 수 없다.

변수에 할당된 메모리의 크기는 확장되거나 줄어들지 않는다.

이에 따라 1바이트 크기의 변수에 1을 더하다가 그 값이 0xff를 넘어서게 되면, 0x100이 되는 것이 아니라 0x00이 된다.

이런 현상을 데이터가 넘쳐 유실됐다고 해서 overflow라고 부른다.

1.2 Type Error

변수의 크기보다 큰 값을 대입하려 할 때도 데이터가 유실될 수 있다.

예를 들어 4바이트 크기의 변수에 0x0123456789abcdf를 대입하려 하면, 하위 4바이트인 0x89abcdef만 저장되고 나머지 값은 모두 버려진다.

이런 실수로 발생할 수 있는 Bug가 Type Error이다.

2. Type Error

2.1 자료형

C언어에는 여러 자료형이 있고, 각각의 자료형은 저장할 수 있는 데이터의 크기가 다르며, 저장하는 값의 용도도 정해져 있다.

변수의 자료형을 선언할 때는 변수를 활용하는 동안 담게 될 값의 크기, 용도, 부호 여부 등을 고려해야한다.

Type error는 이러한 고려 없이 부적절한 자료형을 사용했을 때 발생한다.

2.2 Out of Range

2.2.1 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);
}

위의 코드를 보면, main에서의 unsigend int res는 4바이트이다.

하지만, res는 unsigned long long factorial(8바이트)의 값을 반환한다.

그래서 일정 값이 넘어가게 되면, 값이 유실되기 때문에 아래와 같은 결과가 나타난다.

17보다 큰 18을 넣었음에도 불구하고, Factorial 값은 낮은 것을 확인 할 수 있다.

18!=0x16beecca730000 에서 4바이트 크기의 res에 대입하려면, 상위 4바이트는 버려지고, 하위 4바이트인 0xca730000만 res에 들어가기 때문이다.

2.2.2 Out of Range: 부호 반전과 값의 왜곡

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

위의 코드를 보면, main함수에서 n의 자료형 타입이 int이다.(부호있는 정수, 4바이트)

이 n을 unsigned long long factorial 함수로 넘길 때, 인자로 받는 것은 unsigned int n으로 받는다.(부호없는 정수)

이럴 때, main에서 n을 -1을 넣어주면, factorial에서는 unsigned int의 최댓값인 4294967295이 들어가게 된다.

(부호 있는 정수 -1은, 부호가 없는 정수에 매핑될 때는, int의 최댓값인 4294967295이 된다.)

실행을 하면, 결과가 나오지 않는다.

(4294967295번 만큼 반복문을 실행하게 되어서 시간도 오래걸리고, 연산도 제대로 된 결과가 안나온다.

2.2.3 Out of Range와 Buffer Overflow

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

위 코드를 보면, BUF_SIZE를 32로 설정하고

int size에 입력을 받는다.

그리고, 버퍼 오버플로우를 막기 위해 size가 32보다 작은지 검사한다.

그리고 buf를 입력받는다.

read함수의 원형을 보면

    ssize_t read(int fd, void *buf, size_t count);

인데, size_t 형은 부호가 없는 변수 형태이다.

그래서, int size에 입력 받을 때, -1을 넣는다면, size_t는 int 형의 최대값이 들어가게 되어, 버퍼 오버플로우가 가능하게 된다.

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

변수의 값이 연산 중에 자료형 범위를 벗어나는 경우,

크기가 커지다가 갑자기 작아지는 현상은 Overflow,

크기가 작아지다가 갑자기 커지는 현상은 Underflow라고 한다.

예제 코드

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

실행 결과

2.2.5 Integer Overflow와 버퍼 오버플로우

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

integer overflow가 힙 버퍼 오버플로우 코드이다.

size 값을 입력받고, size + 1 크기의 버퍼를 할당한다. 그리고 그 버퍼에 size만큼 사용자 입력을 받는다.

만약 사용자가 size에 unsigned int의 최댓값인 4294967295을 입력하면,

integer overflow로 인해 size+1은 0이 된다.

이 값이 malloc에 전달되면, malloc은 최소 할당 크기인 32바이트만큼 청크를 할당해준다.

read 함수는 size 값을 그대로 사용한다. 따라서 32바이트 크기의 청크에 4294967295만큼 쓸 수 있는, 힙 버퍼 오버플로우가 발생한다.

마치며

Type Overflow: 변수가 저장할 수 있는 최댓값을 넘어서서 최솟값이 되는 버그

Type Underflow: 변수가 저장할 수 있는 최솟값보다 작아 최댓값이 되는 버그

Most Significant Bit(MSB): 데이터의 최상위 비트, 부호를 표현하기 위해 사용됨

Reference

https://dreamhack.io/lecture/courses/118

profile
보안 공부를 하는 학생입니다.

0개의 댓글