ref : dreamhack.io
얼마전에 integer overflow를 마주했었다. 너무 대충 아는 것 같아서 드림핵 강의를 보면서 정리해보려고 한다.
정수의 범위
- (signed) char : -2^7 ~ 2^7-1
- unsigned char : 0 ~ 2^8-1 (only 양수)
- (signed) short : -2^15 ~ 2^15-1
- unsigned short : 0 ~ 2^16-1 (only 양수)
- (signed) int : -2^31 ~ 2^31-1
- unsigned int : 0 ~ 2^32-1 (only 양수)
- (signed) long long : -2^63 ~ 2^63-1
- unsigned long long : 0 ~ 2^64-1 (only 양수)
size_t와 long 자료형은 아키텍쳐에 따라 표현할 수 있는 수의 범위가 달라진다.
long 자료형
32bit -> int와 동일 (-2^31 ~ 2^31-1)
64bit -> long long과 동일 (-2^63 ~ 2^63-1)
size_t 자료형
32bit -> unsigned int와 동일 0 ~ 2^32-1 (only 양수)
64bit -> u
size_t란??
부호 없는 정수형(unsigned integer)으로 sizeof, alignof, offsetof의 반환값일반적으로 배열 인덱싱 및 루프 계산, 문자열이나 메모리의 사이즈를 나타낼 때 사용)size_t는 32bit OS에서는 "unsigned 32bit integer"이고, 64bit OS에서는 "unsigned 64bit integer"이다. 그러나 "unsigned int" 또는 "int"는 64bit OS라고 해서 꼭 64bit정수는 아니다.. 여전히 32bit일 수도 있다. 이것이 size_t와 unsigned int의 차이다.
묵시적 형 변환
연산을 진행할 때 피연산자로 오는 데이터들의 자료형이 서로 다를 경우, 다양한 종류의 형 변환이 일어나게 된다. 이 때, 프로그래머가 자료형을 직접 명시해주지 않는다면 묵시적으로 형 변환이 발생한다.
- 대입 연산 : 대입 연산의 좌변과 우변이 다를 경우 묵시적 형 변환이 발생, 작은 정수 자료형에 큰 정수를 저장하는 경우, 작은 정수의 크기에 맞춰서 상위 바이트가 소멸됨
- 정수 승격 : char이나 short 자료형이 연산될 때. 컴퓨터가 int형을 기반으로 연산하기 때문에 발생
- 피연산자가 불일치할 경우 : int < long < long long < float < double < long double 순으로 변환, 작은 바이트에서 큰 바이트로, 정수에서 실수로 형변환.
eex) int + double => int가 double 형으로 형변환된 후 연산이 진행
Integer Issues
test 1) int-1
//int-1.c
#include <stdio.h>
#include <stdlib.h>
int main(void) {
char *buf;
int len;
printf("Length: ");
scanf("%d", &len);
buf = (char *)malloc(len + 1);
if(!buf) {
printf("Error!");
return -1;
}
read(0, buf, len);
}
len 값을 사용자에게 입력받은 이후 len+1 만큼 메모리를 할당받고 그 포인터를 buf에 저장한다. 그리고 read 함수를 통해 buf에 데이터를 len만큼 입력받는다.
공격자가 len 값으로 -1을 넣으면 어떻게 될까?
len=-1이므로 buf = malloc(0)이 호출된다.
여기서부터 정상적으로 실행이 안될 것 같지만.. 리눅스에서는 malloc의 인자가 0이라면 정상적인 힙 메모리가 반환이 된다. 이후 read(0, buf, -1)이 호출된다.
인자로 전달된 값은 int형 값 -1이고, read함수의 세 번째 인자는 size_t형이다. 따라서 묵시적 형변환이 일어나게 된다.
따라서 아래와 같이 len에는 0xffffffff 즉, 2^32-1 값이 들어가게 된다.
64bit OS이니 read함수의 인자는 RDI, RSI, RDX 에 들어갈 것이다. 이 중에서 RDX에 들어가는 것이 len이다.
지정된 크기의 버퍼를 넘는 데이터를 넣을 수 있어 힙 오버플로우가 발생하게 된다..
test 2) int-2
// int-2.c
char *create_tbl(unsigned int width, unsigned int height, char *row) {
unsigned int n;
int i;
char *buf;
n = width * height;
buf = (char *)malloc(n);
if(!buf)
return NULL;
for(i = 0; i < height; i++)
memcpy(&buf[i * width], row, width);
return buf;
}
width, height 값과 초기화 데이터인 row 포인터를 인자로 받고 테이블을 초기화 한다.
width * height 크기의 테이블을 할당한 후 각 행에 init_row 데이터를 복사한다.
memcpy 함수
#include <string.h> // C++ 에서는 <cstring>
void* memcpy(void* destination, const void* source, size_t num);
source가 가리키는 곳부터 num 바이트 만큼을 destination이 가리키는 곳에 복사한다.
위 test code에서 width, height, n은 전부 unsigned int형의 변수이다. 따라서 width * height가 2^32를 넘어가면 의도치 않은 값이 들어간다.
- unsigned int : 0 ~ 2^32-1 (only 양수)
width가 65536이고 height가 65537이라고 가정해보자.
width * height = 65536 * 65537 = 2^32 + 65536 => 65536
실제로 malloc은 65536 * 65537 크기의 힙이 할당되는 것이 아니라 65536 크기만큼만 할당되게 된다.
하지만 for문의 memcpy 함수에서 루프를 돌면서 메모리를 실제로 할당된 65536 크기를 넘어 65536 * 65537 만큼 계속 복사하기 때문에 버퍼오버플로우가 발생하게 된다.
3) Quiz
몇 번째 line이 취약할까?
length는 입력할 수 있는 값이라고 주석에 적혀있다.
0x8000 크기로 힙에 버퍼를 할당한다.
그리고 if(length < 0 || length + 1 >= MAX_SIZ) 에서 살짝... overflow를 막아주는 척한다. 하지만 실제로 그렇지 않다.
아무튼 read 함수에서 할당한 buf에 length만큼 입력이 가능하다.
length에 int형의 최대값인 0x7FFFFFFF를 넣어보면 어떻게 될까?
length = 0x7FFFFFFF 양수이다.
length + 1 = 0x80000000 int 범위에 넘어가서 음수로 취급된다.
그래서 if(length < 0 || length + 1 >= MAX_SIZ) 이 조건에 해당하지 않아 line13으로 건너뛰어 read함수를 호출하게 된다.
결론은 read(fd, buf, 0x7FFFFFFF) 가 호출되어 힙 오버플로우가 발생하게 된다.
'System Hacking' 카테고리의 다른 글
[HackingCamp 22] 퍼징의 이해 - 장대희님 (0) | 2021.03.02 |
---|---|
힙풍수 (Heap Feng Shui) (2) | 2021.03.02 |
Use-After-Free (UAF) (0) | 2021.01.06 |
Master canary (마스터 카나리) (1) | 2020.12.21 |
one-shot gadget (원샷 가젯) (0) | 2020.11.13 |