다양한 기록

프로그램 버그 예시 본문

보안개론

프로그램 버그 예시

라구넹 2024. 5. 19. 01:16

- 루프

void main() {
	float = 0.1;
    
	while( x != 1.1 ) {
		x = x + 0.1;
 		printf("x = %f\n", x);
		if(x > 3) break;
	}
}

컴퓨터 특성 상 실수 연산은 완전히 정확하지 않음.

저 루프가 운이 좋으면 끝나겠지만 아니면 무한 루프가 될 것

 

- 문법 문제

int k = 1;
int val = 0;

while( k = 10 ) {
	val++;
	k++;
}
printf("k = %d, val = %d\n", k, val);

k == 10 으로 해야 함

 

- 메모리 에러

char buffer[5] = "Great";

int id_seq[3];

id_seq[-1] = 123;
id_seq[1] = 123;
id_seq[2] = 123;
id_seq[3] = 123;
id_seq[4] = 123;

printf("id_seq[4] = %d\n", id_seq[4]);

buffer -> G r e a t \0 로 총 6자라 버퍼가 넘침

id_seq[-1] : id_seq 기준으로 4바이트 전 

id_seq[4] : id_seq[3] 주소 기준으로 4바이트 후

Segmentation fault가 날 수도 있고 안 날수도 있음

 

ex) id_seq[-1]에는 문제가 안 생겨도 id_seq[4]만 문제가 생기는 경우

스택 보호 기법 때문

스택은 위에서 아래로 자라는 형태 -> 한쪽 방향만 검사

-fno-stack-protector 옵션을 컴파일할 때 쓰면 스택 보호 해제 가능

 

- Off-by-one error

int i;
int buffer[5];

for( i = 0; i <= 5; i++ )
	cin >> buffer[i];
int array[] = new int[5];

for(int i = 0; i <= 5; i++)
	System.out.println( array[i] );

< 대신 <= 을 쓴 것으로 인해 사용할 메모리 범위를 벗어나게 됨

Off-by-one : 하나 초과했다는 뜻

 

- 데이터 사이즈

// 64 비트 기준
printf("sizeof(char) = %lu\n", sizeof(char));	// 1
printf("sizeof(char) = %lu\n", sizeof(short));	// 2
printf("sizeof(char) = %lu\n", sizeof(int));	// 4
printf("sizeof(char) = %lu\n", sizeof(long));	// 8

printf("sizeof(char) = %lu\n", sizeof(char *));	// 8
printf("sizeof(char) = %lu\n", sizeof(short *));// 8
printf("sizeof(char) = %lu\n", sizeof(int *));	// 8
printf("sizeof(char) = %lu\n", sizeof(long *));	// 8

%lu : unsigned long 의 형식 지정자

long은 64비트 프로그램에선 8바이트, 32비트 프로그램에선 4바이트

 

64비트 / 32비트

= 워드의 크기(한 사이클에 처리 가능한 데이터의 크기)

= 레지스터의 크기

= 어드레스 버스의 크기

실제로 64비트에서 주소를 64비트로 쓰지는 않고 48비트

64비트 너무 커서 그냥 48비트 씀

언젠가 바뀔수도

 

printf("CHAR_MIN = %10d, %x, %10u\n", CHAR_MIN, CHAR_MIN, CHAR_MIN);
// -128, ffffff80, 4294967168

printf("CHAR_MAX = %10d, %x, %10u\n", CHAR_MAX, CHAR_MAX, CHAR_MAX);
// 127, 7f, 127

printf("UCHAR_MAX = %10d, %x, %10u\n", UCHAR_MAX, UCHAR_MAX, UCHAR_MAX);
// 255, ff, 255

%d : 4바이트 크기 표시 가능

%x : 헥사 데시멀, 바이너리를 보여줌, 4바이트

%u : 언사인드 데시멀, 4바이트

 

캐릭터는 signed, 1바이트 크기

맞지 않는 형식 지정자를 사용하면 문제가 생길 수 있음

 

** CHAR_MIN의 이진수가 ffffff80 으로 보이는 이유

사이즈 안맞으면 최상위 비트를 그대로 복제함 -> 16진수 80 -> 2진수 1000 0000

-> 복제 시 111111......10000000 이 됨

 

이걸 Unsigned로 받아서 읽는다 -> 4294967168로 읽어지는 문제가 발생

 

이걸 예방하기 위해선, hhd, hhx, hhu를 사용해야 함

h는 하프를 뜻하는데, 4바이트의 반의 반이면 1바이트

char의 크기가 1바이트이니 크기를 잘 맞춰주어야 버그가 생기지 않음

 

printf("SHRT_MIN = %10d, %x, %10u\n", SHRT_MIN, SHRT_MIN, SHRT_MIN);
// -32768, ffff8000, 4294934528

printf("SHRT_MAX = %10d, %x, %10u\n", SHRT_MAX, SHRT_MAX, SHRT_MAX);
// 32767, 7fff, 32767

printf("USHRT_MAX = %10d, %x, %10u\n", USHRT_MAX, USHRT_MAX, USHRT_MAX);
// 65535, ffff, 65535

printf("INT_MIN = %10d, %x, %10u\n", INT_MIN, INT_MIN, INT_MIN);
// -2147483648, 80000000, 2147483648

printf("INT_MAX = %10d, %x, %10u\n", INT_MAX, INT_MAX, INT_MAX);
//  2147483647, 7fffffff, 2147483647

printf("UINT_MAX = %10d, %x, %10u\n", UINT_MAX, UINT_MAX, UINT_MAX);
// -1, ffffffff, 4294967295

printf("LLONG_MIN = %20lld, %llx, %20llu\n", LLONG_MIN, LLONG_MIN, LLONG_MIN);
// -9223372036854775808, 8000000000000000, 9223372036854775808

printf("LLONG_MAX = %20lld, %llx, %20llu\n", LLONG_MAX, LLONG_MAX, LLONG_MAX);
// 9223372036854775807, 7fffffffffffffff, 9223372036854775807

printf("ULLONG_MAX = %20lld, %llx, %20llu\n", ULLONG_MAX, ULLONG_MAX, ULLONG_MAX);
// -1, ffffffffffffffff, 18446744073709551615

int는 애초에 4바이트니까 %d, %x, %u에 바이트가 확장되지 않고 잘 표시됨

단, signed를 unsigned로 출력하려는 시도나 반대 경우는 당연히 이상한 결과를 냄


file 명령어

리눅스(우분투)
Mac OS

바이너리 파일의 포맷과 몇 비트 단위인지를 알 수 있음. 바이너리의 포맷을 알면 역공학이 가능

리눅스는 ELF, 윈도우는 PE(Portable Executable format), 안드로이드는 DEX (Dalvik EXcutable format),

ios나 mac OS는 Mach-O

 

pie: position independent code

어느 메모리 주소에서도 실행 가능하다는 뜻

 

gcc -m32 test_32.c -o test_32

파일이 32비트 형식으로 나옴


오버플로우, 언더플로우

pirntf("SHRT_MIN-1 (%10d, %x, %10u)\n", SHRT_MIN-1, SHRT_MIN-1, SHRT_MIN-1);
// -32769, ffff7fff, 4294934527

pirntf("SHRT_MAX+1 (%10d, %x, %10u)\n", SHRT_MAX+1, SHRT_MAX+1, SHRT_MAX+1);
// 32768, 8000, 32768

pirntf("USHRT_MAX+1 (%10d, %x, %10u)\n", USHRT_MAX+1, USHRT_MAX+1, USHRT_MAX+1);
// 65536, 10000, 65536

최소값에서 -1을 하거나, 최대값에서 +1을 하면 언더플로우 / 오버플로우가 발생해야 함

근데 발생 안했음 -> 형식 지정자가 4바이트라 발생하지 않았음

 

pirntf("INT_MIN-1 (%10d, %x, %10u)\n", INT_MIN-1, INT_MIN-1, INT_MIN-1);
// 2147483647, 7fffffff, 2147483647

pirntf("INT_MAX+1 (%10d, %x, %10u)\n", INT_MAX+1, INT_MAX+1, INT_MAX+1);
// -2147483648, 80000000, 2147483648

pirntf("UINT_MAX+1 (%10d, %x, %10u)\n", UINT_MAX+1, UINT_MAX+1, UINT_MAX+1);
// 0, 0, 0

형식 지정자와 크기가 맞으면 오버플로우와 언더플로우가 발생하는게 보임

Unsigned int의 경우 최대치에서 +1 하면 0이 되어버림


IO 관련 문제

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

void main(int argc, char *argv[]) {
   char buf[16] = "Hello";
   
   // 문제 1
   printf("buf = %s, %x, %x, %x\n", buf);
   printf("addr of main()\n", main);
   /*
   *	buf = 1000000, 565e91f4, f7ee2540
   *	addr od main() = 565e91dd
   */
   
   // 문제 2
   strcpy(buf, argv[1]);
   
   // 문제 3
   printf("buf = %s, %x, %x, %x\n", buf);
   
   // 문제 4
   printf("Enter a string: ");
   scanf("%s", buf);
   
   printf("Input string is %s\n", buf);
   printf("End\n");
}

문제1

형식 지정자의 수가 인자의 수와 다름

이러면 pritnf는 버퍼 다음 위치에 있는 스택에 있는 인자를 꺼내옴

 

스택에 저장되는 값

인자->리턴 어드레스->saved ebp->지역변수

** pc는 텍스트 세그먼트의 오프셋을 가리키고 ebp는 스택의 오프셋을 가리킴

** 스택의 탑을 가리키는 건 esp

** pc는 인텔에서는 eip.. pc는 범용적인 용어

 

스택에서 계속 꺼내다 보면 리턴 어드레스의 주소를 알 수 있음

 

문제2

argv[1] 값의 길이가 buf 길이를 넘어버리면 오버플로우

 

문제3

printf의 인자 개수가 안맞아서 스택에서 값을 꺼내오게 됨

 

문제4

입력하는 길이가 buf의 크기를 넘어가면 오버플로우

너무 길게 넣으면 리턴 어드레스까지 침범할 수도 있음

혹은, 입력을 "Hello World %p %p %p %p %p %p" 같이 주면

printf에서 argv[1]의 내용을 출력하게 될 경우 형식 지정자의 수가 안맞아 스택에서 꺼내오게 됨

 

#include <stdio.h>

void main(int argc, char *argv[]) {
    char buf[16] = "Hello";
    
    printf("argv[1] = %s\n", argv[1]);
    
    // 취약
    printf(argv[1]);
    printf("\n");
}

인자 없이 그냥 출력을 하라고 하는 경우

만약 argv[1]가 "%p %p %p %p %p %p" 이면 스택에서 값을 꺼내오게 됨

 

 

#include <stdio.h>

void main(int argc, char *argv[]) {
    char buf[100];
    
    snprintf(buf, sizeof(buf), "%s", argv[1]);
    
    // 취약
    snprintf(buf, sizeof(buf), argv[1]);
    printf("buf = %s\n", buf);
}

sprintf()

출력을 첫 인자로 보냄

argv[1]가 "Hello %s %s %s %s %s %s" 이런 식이면?

%s는 널문자를 만날 때까지 출력함

널 문자가 없으면 굉장히 길게 출력되다가, 너무 길게가서 커널을 건드릴 수 있음

-> 도스 공격 유발 가능


Dangling Pointer (= Null pointer dereference)

int *ptr = NULL;
printf("ptr: %p\n", ptr);

int *p = 0;
*p = 1;

가리키는게 없는데 값을 쓸 수는 없음

 

int length;
char *buf;

scanf("%d", &length);
buff = (char *)malloc(length + 1);
strcpy(buff, "Hello");

혹은, 동적할당을 받아도 저게 NULL이 아니라고 확신할 수는 없음

자리가 없으면 할당을 못해줌