변수의 자료형을 선언할 때는 변수를 활용하는 동안 담게 될 값의 크기, 용도, 부호, 여부를 고려해야 합니다. Type error는 이러한 고려 없이 부적절한 자료형을 사용했을 때 발생합니다.
// 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);
}
kali@kali ~/dreamhack/Logical_Bug ./out_of_range
Input integer n: 17
Factorial of N: 4006445056
kali@kali ~/dreamhack/Logical_Bug ./out_of_range
Input integer n: 18
Factorial of N: 3396534272
코드를 실행하고, 값을 입력하다 보면 18에서 값이 갑자기 작아지는 것을 확인할 수 있습니다.
18!=0x16beecca730000인데 이를 4바이트 크기의 res에 대입하려고 해서 상위 4바이트는 버려지고, 하위 4바이트인 0xca730000만 저장되서 그렇습니다.
// 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);
}
kali@kali ~/dreamhack/Logical_Bug ./oor_signflip
Input integer n: -1
입력으로 -1을 주면 시간이 지나도 결과가 나오지 않습니다.
그 이유는 -1을 입력하면 2의 보수에 의해 0xffffffff가 저장됩니다. 이 값은 factorial 함수의 인자로 들어갈때 부호가 없는 정수인 4294967295로 전달되고, 결국 반복문이 4294967295번 실행되게 되서 시간이 지나도 결과가 나오지 않게 됩니다.
// 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;
}
ssize_t read(int fd, void *buf, size_t count);
read 함수의 세 번째 인자는 부호가 없는 size_t 형이므로, 음수를 전달하면 매우 큰 수로 해석됩니다.
kali@kali ~/dreamhack/Logical_Bug ./oor_bof
Input length: -1
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAa
[2] 31135 segmentation fault (core dumped) ./oor_bof
size에 -1을 입력하고 32 바이트보다 큰 데이터를 입력하면 스택 버퍼 오버플로우가 발생합니다.
*64 비트로 컴파일 했을 때는 안되는 이유*
64비트 환경에서 -1은 0xffffffffffffffff입니다. 이를 부호가 없는 size_t로 환산하면 18446744073709551615가 됩니다. read 함수는 count 값에 이렇게 큰 값이 들어오면 아무런 동작도 하지 않고 에러값을 반환합니다.
변수 값이 연산 중에 자료형의 범위를 벗어나면, 갑자기 크기가 작아지거나 커지는 현상이 발생하는데, 이런 현상을 Type Overflow/Underflow라고 합니다. 정수 자료형을 대상으로 발생하면 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;
}
kali@kali ~/dreamhack/Logical_Bug ./integer_example
0
-2147483648
4294967295
2147483647
// 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에 unsigned int의 최댓값인 4294967295을 입력하면, integer overflow로 인해 size + 1은 0이 됩니다. malloc에 0이 들어가게 되면 malloc은 최소 할당 크기인 32바이트만큼 청크를 할당해줍니다. 반면 read 함수는 size 값인 4294967295를 그대로 사용합니다. 따라서 힙 버퍼 오버플로우가 발생합니다.