[Pwnable] 19. Logical Bug: Type Error

Wonder_Land🛕·2022년 11월 22일
0

[Pwnable]

목록 보기
19/21
post-thumbnail

[Reference] : 위 글은 다음 내용을 제가 공부한 후, 인용∙참고∙정리하여 만들어진 게시글입니다.


  1. 서론
  2. 타입 에러
  3. Q&A
  4. 마치며

1. 서론

자료형은 변수의 크기를 정의하고, 용도를 암시합니다.
자료형이 담고 있는 정보는 컴파일러에도 전달됩니다.
컴파일러는 변수의 자료형을 참고하여 변수에 관한 코드를 생성합니다.
그리고 각 변수에 대한 연산은 그 메모리 공간을 대상으로 이뤄집니다.

한 번 정의된 변수의 자료형은 바꿀 수 없습니다.
즉, 변수에 할당된 메모리의 크기는 확장되거나 줄어들지 않습니다.
이에 따라, 1바이트 크기의 변수에 1을 더하다가 그 값이 0xff가 되면 0x100이 아닌, 0x00이 됩니다.
이러한 현상을 데이터가 넘쳐서 유실됐다고 하여 overflow라고 합니다.

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


2. 타입 에러

1) 자료형

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

주의해야 할 점은, 같은 자료형이라도 운영체제에 따라 크기가 달라질 수 있다는 것입니다.
예를 들어 long은 32비트 운영체제에서는 4바이트의 크기를 갖기만, 64비트에서는 8바이트의 크기를 갖습니다.

변수의 자료형을 선언할 때는 변수를 활용하는 동안 담게 될 값의 크기, 용도, 부호 여부를 고려해야합니다.
Type Error는 이러한 고려 없이 부적절한 자료형을 사용했을 때 발생합니다.


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);
}
$ ./out_of_range
Input integer n: 17
Factorial of N: 4006445056
$ ./out_of_range
Input integer n: 18
Factorial of N: 3396534272

실행 결과를 보게 되면 18에서 값이 갑자기 작아지는 것을 알 수 있습니다.

코드 상에서, 양수만을 곱했는데 값이 작아진 이유는 res에 저장될 수 있는 범위보다 훨씬 큰 값을 저장하려 했기 때문입니다.

18!=0x16beecca730000입니다. 이를 4바이트 크기의 res에 대입하려 하면, 상위 4바이트는 버려지고, 하위 4바이트인 0xca730000만 옮겨집니다. 이는 출력된 값과 정확히 일치합니다.

이처럼, 변수에 어떤 값을 대입할 때, 그 값이 변수에 저장될 수 있는 범위를 벗어나면, 저장할 수 있는 만큼만 저장하고 나머지는 모두 유실됩니다.


3) 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);
}
$ ./oor_signflip
Input integer n: -1

위의 코드는 앞의 예제에서 main 함수의 변수 n의 자료형이 int로 바뀌었습니다.
이는 입력값으로 음수를 입력하면 main함수의 if문을 우회할 수 있음을 의미합니다.

코드를 실행하고 -1을 입력하면 아무리 기다려도 결과가 나오지 않습니다.

int n-1을 저장하면, n의 메모리 공간에 저장되는 값은 0xffffffff입니다.
그런데 factorial함수는 unsigned int n을 인자로 받으므로, 이 값은 부호 없는 정수인 4294967295로 전달되고, 결국4294967295번 반복문을 실행하게 됩니다. 당연히, 시간이 오래 걸릴 뿐만 아니라 값이 너무 커져서 연산도 제대로 이뤄지지 않습니다.

이런 문제를 예방하려면 양수로만 쓰일 값에 반드시 unsigned를 붙이는 습관을 들여야 합니다.


4) Out of Range와 버퍼 오버플로우

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

위의 코드는, 잘못된 자료형의 사용이 스택 버퍼 오버플로우로 이어지는 코드입니다.

버퍼 플로우를 막기 위해 size가 32보다 작은지 검사합니다.
그러나 sizeint형이므로 음수를 전달한다면 검사를 우회할 수 있습니다.
이 때, read함수의 세 번째 인자는 부호가 없는 size_t형이므로, 음수로 전달하면 매우 큰 수로 해석됩니다.
( ※ size_t는 부호 없는 정수를 저장하며, 32비트 운영체제에서는 4바이트, 64비트에서는 8바이트의 크기를 가집니다.)

실제로 size-1을 입력하고 32바이트보다 큰 데이터를 입력하면 다음과 같이 스택 버퍼 오버플로우가 발생합니다.

$ ./oor_bof
Input length: -1
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
*** stack smashing detected ***: <unknown> terminated
Aborted (core dumped)

64비트에서 컴파일했을 때는 동작이 이뤄지지 않습니다.
64비트 환경에서 -10xffffffffffffffff입니다. size_t는 64 비트 환경에서 8바이트 크기의 부호 없는 정수를 나타내므로, size_t로 환산하면 -118446744073709551615이 됩니다. read함수는 count의 값으로 이렇게 큰 값이 들어오면 아무런 동작도 하지 않고 에러값을 반환합니다.


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

변수의 값이 연산 중에 자료형의 범위를 벗어나면, 갑자기 크기가 작아지거나 커지는 현상이 발생하는데,
이를 'Type Overflow / Underflow'라고 합니다.
만약, 정수 자료형을 대상으로 발생하면 TypeInteger를 넣어서 'Integer 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;
}
$ ./integer_example
0
-2147483648
4294967295
2147483647

오버플로우가 발생하면 자료형이 표현할 수 있는 최솟값이 되며, 언더플로우가 발생하면 최댓값이 됩니다.


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

위의 코드는 intger overflow가 힙 버퍼 오버플로우로 이어지는 코드입니다.
사용자로부터 size값을 입력받고, size + 1의 크기의 버퍼를 할당합니다. 그리고 그 버퍼에 size만큼 입력을 받습니다.

만약 사용자가 sizeunsigned int의 최댓값인 4294967295을 입력하면, integer overflow로 인해, size + 10이 됩니다.
이 값이 malloc에 전달되면, malloc은 최소 할당 크기인 32바이트만큼 청크를 할당해줍니다.
반면, read함수는 size값을 그대로 사용합니다.
따라서 32바이트 크기의 청크에 4294967295만큼 값을 쓸 수 있는, 힙 버퍼 오버플로우가 발생하게 됩니다.


5. Q&A

-


6. 마치며

-

[Reference] : 위 글은 다음 내용을 제가 공부한 후, 인용∙참고∙정리하여 만들어진 게시글입니다.

profile
아무것도 모르는 컴공 학생의 Wonder_Land

0개의 댓글